/* $NetBSD: hdaudio.c,v 1.18 2022/04/07 19:33:37 andvar Exp $ */

/*
* Copyright (c) 2009 Precedence Technologies Ltd <[email protected]>
* Copyright (c) 2009 Jared D. McNeill <[email protected]>
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Precedence Technologies Ltd
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. The name of the author may not be used to endorse or promote products
*    derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: hdaudio.c,v 1.18 2022/04/07 19:33:37 andvar Exp $");

#include <sys/types.h>
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/conf.h>
#include <sys/bus.h>
#include <sys/kmem.h>
#include <sys/module.h>

#include "hdaudiovar.h"
#include "hdaudioreg.h"
#include "hdaudioio.h"
#include "hdaudio_verbose.h"
#include "hdaudiodevs.h"

/* #define      HDAUDIO_DEBUG */

#define HDAUDIO_RESET_TIMEOUT   5000
#define HDAUDIO_CORB_TIMEOUT    1000
#define HDAUDIO_RIRB_TIMEOUT    5000

#define HDAUDIO_CODEC_DELAY     1000    /* spec calls for 250 */

dev_type_open(hdaudioopen);
dev_type_close(hdaudioclose);
dev_type_ioctl(hdaudioioctl);

const struct cdevsw hdaudio_cdevsw = {
       .d_open = hdaudioopen,
       .d_close = hdaudioclose,
       .d_read = noread,
       .d_write = nowrite,
       .d_ioctl = hdaudioioctl,
       .d_stop = nostop,
       .d_tty = notty,
       .d_poll = nopoll,
       .d_mmap = nommap,
       .d_kqfilter = nokqfilter,
       .d_discard = nodiscard,
       .d_flag = D_OTHER
};

extern struct cfdriver hdaudio_cd;

#define HDAUDIOUNIT(x)  minor((x))

static void
hdaudio_stream_init(struct hdaudio_softc *sc, int nis, int nos, int nbidir)
{
       int i, cnt = 0;

       for (i = 0; i < nis && cnt < HDAUDIO_MAX_STREAMS; i++) {
               sc->sc_stream[cnt].st_host = sc;
               sc->sc_stream[cnt].st_enable = true;
               sc->sc_stream[cnt].st_shift = cnt;
               sc->sc_stream[cnt++].st_type = HDAUDIO_STREAM_ISS;
       }
       for (i = 0; i < nos && cnt < HDAUDIO_MAX_STREAMS; i++) {
               sc->sc_stream[cnt].st_host = sc;
               sc->sc_stream[cnt].st_enable = true;
               sc->sc_stream[cnt].st_shift = cnt;
               sc->sc_stream[cnt++].st_type = HDAUDIO_STREAM_OSS;
       }
       for (i = 0; i < nbidir && cnt < HDAUDIO_MAX_STREAMS; i++) {
               sc->sc_stream[cnt].st_host = sc;
               sc->sc_stream[cnt].st_enable = true;
               sc->sc_stream[cnt].st_shift = cnt;
               sc->sc_stream[cnt++].st_type = HDAUDIO_STREAM_BSS;
       }

       for (i = 0; i < cnt; i++)
               hdaudio_stream_stop(&sc->sc_stream[i]);

       sc->sc_stream_mask = 0;
}

static void
hdaudio_codec_init(struct hdaudio_softc *sc)
{
       int i;

       for (i = 0; i < HDAUDIO_MAX_CODECS; i++) {
               sc->sc_codec[i].co_addr = i;
               sc->sc_codec[i].co_host = sc;
       }
}

static void
hdaudio_init(struct hdaudio_softc *sc)
{
       const uint8_t vmaj = hda_read1(sc, HDAUDIO_MMIO_VMAJ);
       const uint8_t vmin = hda_read1(sc, HDAUDIO_MMIO_VMIN);
       const uint16_t gcap = hda_read2(sc, HDAUDIO_MMIO_GCAP);
       const int nis = HDAUDIO_GCAP_ISS(gcap);
       const int nos = HDAUDIO_GCAP_OSS(gcap);
       const int nbidir = HDAUDIO_GCAP_BSS(gcap);
       const int nsdo = HDAUDIO_GCAP_NSDO(gcap);
       const int addr64 = HDAUDIO_GCAP_64OK(gcap);

       hda_print(sc, "HDA ver. %d.%d, OSS %d, ISS %d, BSS %d, SDO %d%s\n",
           vmaj, vmin, nos, nis, nbidir, nsdo, addr64 ? ", 64-bit" : "");

       /* Initialize codecs and streams */
       hdaudio_codec_init(sc);
       hdaudio_stream_init(sc, nis, nos, nbidir);
}

static int
hdaudio_codec_probe(struct hdaudio_softc *sc)
{
       uint16_t statests;
       int codecid;

       statests = hda_read2(sc, HDAUDIO_MMIO_STATESTS);
       for (codecid = 0; codecid < HDAUDIO_MAX_CODECS; codecid++)
               if (statests & (1 << codecid))
                       sc->sc_codec[codecid].co_valid = true;
       hda_write2(sc, HDAUDIO_MMIO_STATESTS, statests);

       return statests;
}

int
hdaudio_dma_alloc(struct hdaudio_softc *sc, struct hdaudio_dma *dma,
   int flags)
{
       int err;

       KASSERT(dma->dma_size > 0);

       err = bus_dmamem_alloc(sc->sc_dmat, dma->dma_size, 128, 0,
           dma->dma_segs, sizeof(dma->dma_segs) / sizeof(dma->dma_segs[0]),
           &dma->dma_nsegs, BUS_DMA_WAITOK);
       if (err)
               return err;
       err = bus_dmamem_map(sc->sc_dmat, dma->dma_segs, dma->dma_nsegs,
           dma->dma_size, &dma->dma_addr, BUS_DMA_WAITOK | flags);
       if (err)
               goto free;
       err = bus_dmamap_create(sc->sc_dmat, dma->dma_size, dma->dma_nsegs,
           dma->dma_size, 0, BUS_DMA_WAITOK, &dma->dma_map);
       if (err)
               goto unmap;
       err = bus_dmamap_load(sc->sc_dmat, dma->dma_map, dma->dma_addr,
           dma->dma_size, NULL, BUS_DMA_WAITOK | flags);
       if (err)
               goto destroy;

       memset(dma->dma_addr, 0, dma->dma_size);
       bus_dmamap_sync(sc->sc_dmat, dma->dma_map, 0, dma->dma_size,
           BUS_DMASYNC_PREWRITE);

       dma->dma_valid = true;
       return 0;

destroy:
       bus_dmamap_destroy(sc->sc_dmat, dma->dma_map);
unmap:
       bus_dmamem_unmap(sc->sc_dmat, dma->dma_addr, dma->dma_size);
free:
       bus_dmamem_free(sc->sc_dmat, dma->dma_segs, dma->dma_nsegs);

       dma->dma_valid = false;
       return err;
}

void
hdaudio_dma_free(struct hdaudio_softc *sc, struct hdaudio_dma *dma)
{
       if (dma->dma_valid == false)
               return;
       bus_dmamap_unload(sc->sc_dmat, dma->dma_map);
       bus_dmamap_destroy(sc->sc_dmat, dma->dma_map);
       bus_dmamem_unmap(sc->sc_dmat, dma->dma_addr, dma->dma_size);
       bus_dmamem_free(sc->sc_dmat, dma->dma_segs, dma->dma_nsegs);
       dma->dma_valid = false;
}

static void
hdaudio_corb_enqueue(struct hdaudio_softc *sc, int addr, int nid,
   uint32_t control, uint32_t param)
{
       uint32_t *corb = DMA_KERNADDR(&sc->sc_corb);
       uint32_t verb;
       uint16_t corbrp;
       int wp;

       /* Build command */
       verb = (addr << 28) | (nid << 20) | (control << 8) | param;

       /* Fetch and update write pointer */
       corbrp = hda_read2(sc, HDAUDIO_MMIO_CORBWP);
       wp = (corbrp & 0xff) + 1;
       if (wp >= (sc->sc_corb.dma_size / sizeof(*corb)))
               wp = 0;

       /* Enqueue command */
       bus_dmamap_sync(sc->sc_dmat, sc->sc_corb.dma_map, 0,
           sc->sc_corb.dma_size, BUS_DMASYNC_POSTWRITE);
       corb[wp] = verb;
       bus_dmamap_sync(sc->sc_dmat, sc->sc_corb.dma_map, 0,
           sc->sc_corb.dma_size, BUS_DMASYNC_PREWRITE);

       /* Commit updated write pointer */
       hda_write2(sc, HDAUDIO_MMIO_CORBWP, wp);
}

static void
hdaudio_rirb_unsol(struct hdaudio_softc *sc, struct rirb_entry *entry)
{
       struct hdaudio_codec *co;
       struct hdaudio_function_group *fg;
       uint8_t codecid = RIRB_CODEC_ID(entry);
       unsigned int i;

       if (codecid >= HDAUDIO_MAX_CODECS) {
               hda_error(sc, "unsol: codec id 0x%02x out of range\n", codecid);
               return;
       }
       co = &sc->sc_codec[codecid];
       if (sc->sc_codec[codecid].co_valid == false) {
               hda_error(sc, "unsol: codec id 0x%02x not valid\n", codecid);
               return;
       }

       for (i = 0; i < co->co_nfg; i++) {
               fg = &co->co_fg[i];
               if (fg->fg_device && fg->fg_unsol)
                       fg->fg_unsol(fg->fg_device, entry->resp);
       }
}

static uint32_t
hdaudio_rirb_dequeue(struct hdaudio_softc *sc, bool unsol)
{
       uint16_t rirbwp;
       uint64_t *rirb = DMA_KERNADDR(&sc->sc_rirb);
       struct rirb_entry entry;
       int retry;

       for (;;) {
               retry = HDAUDIO_RIRB_TIMEOUT;

               rirbwp = hda_read2(sc, HDAUDIO_MMIO_RIRBWP);
               while (--retry > 0 && (rirbwp & 0xff) == sc->sc_rirbrp) {
                       if (unsol) {
                               /* don't wait for more unsol events */
                               hda_trace(sc, "unsol: rirb empty\n");
                               return 0xffffffff;
                       }
                       hda_delay(10);
                       rirbwp = hda_read2(sc, HDAUDIO_MMIO_RIRBWP);
               }
               if (retry == 0) {
                       hda_error(sc, "RIRB timeout\n");
                       return 0xffffffff;
               }

               sc->sc_rirbrp++;
               if (sc->sc_rirbrp >= (sc->sc_rirb.dma_size / sizeof(*rirb)))
                       sc->sc_rirbrp = 0;

               bus_dmamap_sync(sc->sc_dmat, sc->sc_rirb.dma_map, 0,
                   sc->sc_rirb.dma_size, BUS_DMASYNC_POSTREAD);
               entry = *(struct rirb_entry *)&rirb[sc->sc_rirbrp];
               bus_dmamap_sync(sc->sc_dmat, sc->sc_rirb.dma_map, 0,
                   sc->sc_rirb.dma_size, BUS_DMASYNC_PREREAD);

               hda_trace(sc, "%s: response %08X %08X\n",
                   unsol ? "unsol" : "cmd  ",
                   entry.resp, entry.resp_ex);

               if (RIRB_UNSOL(&entry)) {
                       hdaudio_rirb_unsol(sc, &entry);
                       continue;
               }

               return entry.resp;
       }
}

uint32_t
hdaudio_command(struct hdaudio_codec *co, int nid, uint32_t control,
   uint32_t param)
{
       uint32_t result;
       struct hdaudio_softc *sc = co->co_host;
       mutex_enter(&sc->sc_corb_mtx);
       result = hdaudio_command_unlocked(co, nid, control, param);
       mutex_exit(&sc->sc_corb_mtx);
       return result;
}

uint32_t
hdaudio_command_unlocked(struct hdaudio_codec *co, int nid, uint32_t control,
   uint32_t param)
{
       struct hdaudio_softc *sc = co->co_host;
       uint32_t result;

       hda_trace(sc, "cmd  : request %08X %08X (%02X)\n",
           control, param, nid);
       hdaudio_corb_enqueue(sc, co->co_addr, nid, control, param);
       result = hdaudio_rirb_dequeue(sc, false);

       /* Clear response interrupt status */
       hda_write1(sc, HDAUDIO_MMIO_RIRBSTS, hda_read1(sc, HDAUDIO_MMIO_RIRBSTS));

       return result;
}

static int
hdaudio_corb_setsize(struct hdaudio_softc *sc)
{
       uint8_t corbsize;
       bus_size_t bufsize = 0;

       /*
        * The size of the CORB is programmable to 2, 16, or 256 entries
        * by using the CORBSIZE register. Choose a size based on the
        * controller capabilities, preferring a larger size when possible.
        */
       corbsize = hda_read1(sc, HDAUDIO_MMIO_CORBSIZE);
       corbsize &= ~0x3;
       if ((corbsize >> 4) & 0x4) {
               corbsize |= 0x2;
               bufsize = 1024;
       } else if ((corbsize >> 4) & 0x2) {
               corbsize |= 0x1;
               bufsize = 64;
       } else if ((corbsize >> 4) & 0x1) {
               corbsize |= 0x0;
               bufsize = 8;
       } else {
               hda_error(sc, "couldn't configure CORB size\n");
               return ENXIO;
       }

#if defined(HDAUDIO_DEBUG)
       hda_print(sc, "using %d byte CORB (cap %X)\n",
           (int)bufsize, corbsize >> 4);
#endif

       sc->sc_corb.dma_size = bufsize;
       sc->sc_corb.dma_sizereg = corbsize;

       return 0;
}

static int
hdaudio_corb_config(struct hdaudio_softc *sc)
{
       uint32_t corbubase, corblbase;
       uint16_t corbrp;
       int retry = HDAUDIO_CORB_TIMEOUT;

       /* Program command buffer base address and size */
       corblbase = (uint32_t)DMA_DMAADDR(&sc->sc_corb);
       corbubase = (uint32_t)(((uint64_t)DMA_DMAADDR(&sc->sc_corb)) >> 32);
       hda_write4(sc, HDAUDIO_MMIO_CORBLBASE, corblbase);
       hda_write4(sc, HDAUDIO_MMIO_CORBUBASE, corbubase);
       hda_write1(sc, HDAUDIO_MMIO_CORBSIZE, sc->sc_corb.dma_sizereg);

       /* Clear the read and write pointers */
       hda_write2(sc, HDAUDIO_MMIO_CORBRP, HDAUDIO_CORBRP_RP_RESET);
       hda_write2(sc, HDAUDIO_MMIO_CORBRP, 0);
       do {
               hda_delay(10);
               corbrp = hda_read2(sc, HDAUDIO_MMIO_CORBRP);
       } while (--retry > 0 && (corbrp & HDAUDIO_CORBRP_RP_RESET) != 0);
       if (retry == 0) {
               hda_error(sc, "timeout resetting CORB\n");
               return ETIME;
       }
       hda_write2(sc, HDAUDIO_MMIO_CORBWP, 0);

       return 0;
}

static int
hdaudio_corb_stop(struct hdaudio_softc *sc)
{
       uint8_t corbctl;
       int retry = HDAUDIO_CORB_TIMEOUT;

       /* Stop the CORB if necessary */
       corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
       if (corbctl & HDAUDIO_CORBCTL_RUN) {
               corbctl &= ~HDAUDIO_CORBCTL_RUN;
               hda_write1(sc, HDAUDIO_MMIO_CORBCTL, corbctl);
               do {
                       hda_delay(10);
                       corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
               } while (--retry > 0 && (corbctl & HDAUDIO_CORBCTL_RUN) != 0);
               if (retry == 0) {
                       hda_error(sc, "timeout stopping CORB\n");
                       return ETIME;
               }
       }

       return 0;
}

static int
hdaudio_corb_start(struct hdaudio_softc *sc)
{
       uint8_t corbctl;
       int retry = HDAUDIO_CORB_TIMEOUT;

       /* Start the CORB if necessary */
       corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
       if ((corbctl & HDAUDIO_CORBCTL_RUN) == 0) {
               corbctl |= HDAUDIO_CORBCTL_RUN;
               hda_write1(sc, HDAUDIO_MMIO_CORBCTL, corbctl);
               do {
                       hda_delay(10);
                       corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
               } while (--retry > 0 && (corbctl & HDAUDIO_CORBCTL_RUN) == 0);
               if (retry == 0) {
                       hda_error(sc, "timeout starting CORB\n");
                       return ETIME;
               }
       }

       return 0;
}

static int
hdaudio_rirb_stop(struct hdaudio_softc *sc)
{
       uint8_t rirbctl;
       int retry = HDAUDIO_RIRB_TIMEOUT;

       /* Stop the RIRB if necessary */
       rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
       if (rirbctl & (HDAUDIO_RIRBCTL_RUN|HDAUDIO_RIRBCTL_ROI_EN)) {
               rirbctl &= ~HDAUDIO_RIRBCTL_RUN;
               rirbctl &= ~HDAUDIO_RIRBCTL_ROI_EN;
               hda_write1(sc, HDAUDIO_MMIO_RIRBCTL, rirbctl);
               do {
                       hda_delay(10);
                       rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
               } while (--retry > 0 && (rirbctl & HDAUDIO_RIRBCTL_RUN) != 0);
               if (retry == 0) {
                       hda_error(sc, "timeout stopping RIRB\n");
                       return ETIME;
               }
       }

       return 0;
}

static int
hdaudio_rirb_start(struct hdaudio_softc *sc)
{
       uint8_t rirbctl;
       int retry = HDAUDIO_RIRB_TIMEOUT;

       /* Set the RIRB interrupt count */
       hda_write2(sc, HDAUDIO_MMIO_RINTCNT, 1);

       /* Start the RIRB */
       rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
       rirbctl |= HDAUDIO_RIRBCTL_RUN;
       rirbctl |= HDAUDIO_RIRBCTL_INT_EN;
       hda_write1(sc, HDAUDIO_MMIO_RIRBCTL, rirbctl);
       do {
               hda_delay(10);
               rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
       } while (--retry > 0 && (rirbctl & HDAUDIO_RIRBCTL_RUN) == 0);
       if (retry == 0) {
               hda_error(sc, "timeout starting RIRB\n");
               return ETIME;
       }

       return 0;
}

static int
hdaudio_rirb_setsize(struct hdaudio_softc *sc)
{
       uint8_t rirbsize;
       bus_size_t bufsize = 0;

       /*
        * The size of the RIRB is programmable to 2, 16, or 256 entries
        * by using the RIRBSIZE register. Choose a size based on the
        * controller capabilities, preferring a larger size when possible.
        */
       rirbsize = hda_read1(sc, HDAUDIO_MMIO_RIRBSIZE);
       rirbsize &= ~0x3;
       if ((rirbsize >> 4) & 0x4) {
               rirbsize |= 0x2;
               bufsize = 2048;
       } else if ((rirbsize >> 4) & 0x2) {
               rirbsize |= 0x1;
               bufsize = 128;
       } else if ((rirbsize >> 4) & 0x1) {
               rirbsize |= 0x0;
               bufsize = 16;
       } else {
               hda_error(sc, "couldn't configure RIRB size\n");
               return ENXIO;
       }

#if defined(HDAUDIO_DEBUG)
       hda_print(sc, "using %d byte RIRB (cap %X)\n",
           (int)bufsize, rirbsize >> 4);
#endif

       sc->sc_rirb.dma_size = bufsize;
       sc->sc_rirb.dma_sizereg = rirbsize;

       return 0;
}

static int
hdaudio_rirb_config(struct hdaudio_softc *sc)
{
       uint32_t rirbubase, rirblbase;

       /* Program command buffer base address and size */
       rirblbase = (uint32_t)DMA_DMAADDR(&sc->sc_rirb);
       rirbubase = (uint32_t)(((uint64_t)DMA_DMAADDR(&sc->sc_rirb)) >> 32);
       hda_write4(sc, HDAUDIO_MMIO_RIRBLBASE, rirblbase);
       hda_write4(sc, HDAUDIO_MMIO_RIRBUBASE, rirbubase);
       hda_write1(sc, HDAUDIO_MMIO_RIRBSIZE, sc->sc_rirb.dma_sizereg);

       /* Clear the write pointer */
       hda_write2(sc, HDAUDIO_MMIO_RIRBWP, HDAUDIO_RIRBWP_WP_RESET);
       sc->sc_rirbrp = 0;

       return 0;
}

static int
hdaudio_reset(struct hdaudio_softc *sc)
{
       int retry = HDAUDIO_RESET_TIMEOUT;
       uint32_t gctl;
       int err;

       if ((err = hdaudio_rirb_stop(sc)) != 0) {
               hda_error(sc, "couldn't reset because RIRB is busy\n");
               return err;
       }
       if ((err = hdaudio_corb_stop(sc)) != 0) {
               hda_error(sc, "couldn't reset because CORB is busy\n");
               return err;
       }

       /* Disable wake events */
       hda_write2(sc, HDAUDIO_MMIO_WAKEEN, 0);

       /* Disable interrupts */
       hda_write4(sc, HDAUDIO_MMIO_INTCTL, 0);

       /* Clear state change status register */
       hda_write2(sc, HDAUDIO_MMIO_STATESTS,
           hda_read2(sc, HDAUDIO_MMIO_STATESTS));
       hda_write1(sc, HDAUDIO_MMIO_RIRBSTS,
           hda_read1(sc, HDAUDIO_MMIO_RIRBSTS));

       /* Put the controller into reset state */
       gctl = hda_read4(sc, HDAUDIO_MMIO_GCTL);
       gctl &= ~HDAUDIO_GCTL_CRST;
       hda_write4(sc, HDAUDIO_MMIO_GCTL, gctl);
       do {
               hda_delay(10);
               gctl = hda_read4(sc, HDAUDIO_MMIO_GCTL);
       } while (--retry > 0 && (gctl & HDAUDIO_GCTL_CRST) != 0);
       if (retry == 0) {
               hda_error(sc, "timeout entering reset state\n");
               return ETIME;
       }

       hda_delay(1000);

       /* Now the controller is in reset state, so bring it out */
       retry = HDAUDIO_RESET_TIMEOUT;
       hda_write4(sc, HDAUDIO_MMIO_GCTL, gctl | HDAUDIO_GCTL_CRST);
       do {
               hda_delay(10);
               gctl = hda_read4(sc, HDAUDIO_MMIO_GCTL);
       } while (--retry > 0 && (gctl & HDAUDIO_GCTL_CRST) == 0);
       if (retry == 0) {
               hda_error(sc, "timeout leaving reset state\n");
               return ETIME;
       }

       hda_delay(2000);

       /* Accept unsolicited responses */
       hda_write4(sc, HDAUDIO_MMIO_GCTL, gctl | HDAUDIO_GCTL_UNSOL_EN);

       return 0;
}

static void
hdaudio_intr_enable(struct hdaudio_softc *sc)
{
       hda_write4(sc, HDAUDIO_MMIO_INTSTS,
           hda_read4(sc, HDAUDIO_MMIO_INTSTS));
       hda_write4(sc, HDAUDIO_MMIO_INTCTL,
           HDAUDIO_INTCTL_GIE | HDAUDIO_INTCTL_CIE);
}

static void
hdaudio_intr_disable(struct hdaudio_softc *sc)
{
       hda_write4(sc, HDAUDIO_MMIO_INTCTL, 0);
}

static int
hdaudio_config_print(void *opaque, const char *pnp)
{
       prop_dictionary_t dict = opaque;
       uint8_t fgtype, nid;
       uint16_t vendor, product;
       const char *type = "unknown";

       prop_dictionary_get_uint8(dict, "function-group-type", &fgtype);
       prop_dictionary_get_uint8(dict, "node-id", &nid);
       prop_dictionary_get_uint16(dict, "vendor-id", &vendor);
       prop_dictionary_get_uint16(dict, "product-id", &product);
       if (pnp) {
               if (fgtype == HDAUDIO_GROUP_TYPE_AFG)
                       type = "hdafg";
               else if (fgtype == HDAUDIO_GROUP_TYPE_VSM_FG)
                       type = "hdvsmfg";

               aprint_normal("%s at %s", type, pnp);
       }
       aprint_debug(" vendor 0x%04X product 0x%04X nid 0x%02X",
           vendor, product, nid);

       return UNCONF;
}

static void
hdaudio_attach_fg(struct hdaudio_function_group *fg, prop_array_t config)
{
       struct hdaudio_codec *co = fg->fg_codec;
       struct hdaudio_softc *sc = co->co_host;
       prop_dictionary_t args = prop_dictionary_create();
       uint64_t fgptr = (vaddr_t)fg;
       int locs[1];

       prop_dictionary_set_uint8(args, "function-group-type", fg->fg_type);
       prop_dictionary_set_uint64(args, "function-group", fgptr);
       prop_dictionary_set_uint8(args, "node-id", fg->fg_nid);
       prop_dictionary_set_uint16(args, "vendor-id", fg->fg_vendor);
       prop_dictionary_set_uint16(args, "product-id", fg->fg_product);
       if (config)
               prop_dictionary_set(args, "pin-config", config);

       locs[0] = fg->fg_nid;

       fg->fg_device = config_found(sc->sc_dev, args, hdaudio_config_print,
           CFARGS(.submatch = config_stdsubmatch,
                  .locators = locs));

       prop_object_release(args);
}

static void
hdaudio_codec_attach(struct hdaudio_codec *co)
{
       struct hdaudio_softc *sc = co->co_host;
       struct hdaudio_function_group *fg;
       uint32_t vid, snc, fgrp;
       int starting_node, num_nodes, nid;

       if (co->co_valid == false)
               return;

       vid = hdaudio_command(co, 0, CORB_GET_PARAMETER, COP_VENDOR_ID);
       snc = hdaudio_command(co, 0, CORB_GET_PARAMETER,
           COP_SUBORDINATE_NODE_COUNT);

       /* make sure the vendor and product IDs are valid */
       if (vid == 0xffffffff || vid == 0x00000000)
               return;

#ifdef HDAUDIO_DEBUG
       uint32_t rid = hdaudio_command(co, 0, CORB_GET_PARAMETER,
           COP_REVISION_ID);
       hda_print(sc, "Codec%02X: %04X:%04X HDA %d.%d rev %d stepping %d\n",
           co->co_addr, vid >> 16, vid & 0xffff,
           (rid >> 20) & 0xf, (rid >> 16) & 0xf,
           (rid >> 8) & 0xff, rid & 0xff);
#endif
       starting_node = (snc >> 16) & 0xff;
       num_nodes = snc & 0xff;

       /*
        * If the total number of nodes is 0, there's nothing we can do.
        * This shouldn't happen, so complain about it.
        */
       if (num_nodes == 0) {
               hda_error(sc, "Codec%02X: No subordinate nodes found (%08x)\n",
                   co->co_addr, snc);
               return;
       }

       co->co_nfg = num_nodes;
       co->co_fg = kmem_zalloc(co->co_nfg * sizeof(*co->co_fg), KM_SLEEP);

       for (nid = starting_node; nid < starting_node + num_nodes; nid++) {
               fg = &co->co_fg[nid - starting_node];
               fg->fg_codec = co;
               fg->fg_nid = nid;
               fg->fg_vendor = vid >> 16;
               fg->fg_product = vid & 0xffff;

               fgrp = hdaudio_command(co, nid, CORB_GET_PARAMETER,
                   COP_FUNCTION_GROUP_TYPE);
               switch (fgrp & 0xff) {
               case 0x01:      /* Audio Function Group */
                       fg->fg_type = HDAUDIO_GROUP_TYPE_AFG;
                       break;
               case 0x02:      /* Vendor Specific Modem Function Group */
                       fg->fg_type = HDAUDIO_GROUP_TYPE_VSM_FG;
                       break;
               default:
                       /* Function group type not supported */
                       fg->fg_type = HDAUDIO_GROUP_TYPE_UNKNOWN;
                       break;
               }
               hdaudio_attach_fg(fg, NULL);
       }
}

int
hdaudio_stream_tag(struct hdaudio_stream *st)
{
       int ret = 0;

       switch (st->st_type) {
       case HDAUDIO_STREAM_ISS:
               ret = 1;
               break;
       case HDAUDIO_STREAM_OSS:
               ret = 2;
               break;
       case HDAUDIO_STREAM_BSS:
               ret = 3;
               break;
       }

       return ret;
}

int
hdaudio_attach(device_t dev, struct hdaudio_softc *sc)
{
       int err, i;

       KASSERT(sc->sc_memvalid == true);

       sc->sc_dev = dev;
       mutex_init(&sc->sc_corb_mtx, MUTEX_DEFAULT, IPL_AUDIO);
       mutex_init(&sc->sc_stream_mtx, MUTEX_DEFAULT, IPL_AUDIO);

       /*
        * Put the controller into a known state by entering and leaving
        * CRST as necessary.
        */
       if ((err = hdaudio_reset(sc)) != 0)
               goto fail;

       /*
        * From the spec:
        *
        * Must wait 250us after reading CRST as a 1 before assuming that
        * codecs have all made status change requests and have been
        * registered by the controller.
        *
        * In reality, we need to wait longer than this.
        */
       hda_delay(HDAUDIO_CODEC_DELAY);

       /*
        * Read device capabilities
        */
       hdaudio_init(sc);

       /*
        * Detect codecs
        */
       if (hdaudio_codec_probe(sc) == 0) {
               hda_error(sc, "no codecs found\n");
               err = ENODEV;
               goto fail;
       }

       /*
        * Ensure that the device is in a known state
        */
       hda_write2(sc, HDAUDIO_MMIO_STATESTS, HDAUDIO_STATESTS_SDIWAKE);
       hda_write1(sc, HDAUDIO_MMIO_RIRBSTS,
           HDAUDIO_RIRBSTS_RIRBOIS | HDAUDIO_RIRBSTS_RINTFL);
       hda_write4(sc, HDAUDIO_MMIO_INTSTS,
           hda_read4(sc, HDAUDIO_MMIO_INTSTS));
       hda_write4(sc, HDAUDIO_MMIO_DPLBASE, 0);
       hda_write4(sc, HDAUDIO_MMIO_DPUBASE, 0);

       /*
        * Initialize the CORB. First negotiate a command buffer size,
        * then allocate and configure it.
        */
       if ((err = hdaudio_corb_setsize(sc)) != 0)
               goto fail;
       if ((err = hdaudio_dma_alloc(sc, &sc->sc_corb, BUS_DMA_WRITE)) != 0)
               goto fail;
       if ((err = hdaudio_corb_config(sc)) != 0)
               goto fail;

       /*
        * Initialize the RIRB.
        */
       if ((err = hdaudio_rirb_setsize(sc)) != 0)
               goto fail;
       if ((err = hdaudio_dma_alloc(sc, &sc->sc_rirb, BUS_DMA_READ)) != 0)
               goto fail;
       if ((err = hdaudio_rirb_config(sc)) != 0)
               goto fail;

       /*
        * Start the CORB and RIRB
        */
       if ((err = hdaudio_corb_start(sc)) != 0)
               goto fail;
       if ((err = hdaudio_rirb_start(sc)) != 0)
               goto fail;

       /*
        * Identify and attach discovered codecs
        */
       for (i = 0; i < HDAUDIO_MAX_CODECS; i++)
               hdaudio_codec_attach(&sc->sc_codec[i]);

       /*
        * Enable interrupts
        */
       hdaudio_intr_enable(sc);

fail:
       if (err)
               hda_error(sc, "device driver failed to attach\n");
       return err;
}

int
hdaudio_detach(struct hdaudio_softc *sc, int flags)
{
       int error;

       /* Disable interrupts */
       hdaudio_intr_disable(sc);

       error = config_detach_children(sc->sc_dev, flags);
       if (error != 0) {
               hdaudio_intr_enable(sc);
               return error;
       }

       mutex_destroy(&sc->sc_corb_mtx);
       mutex_destroy(&sc->sc_stream_mtx);

       hdaudio_dma_free(sc, &sc->sc_corb);
       hdaudio_dma_free(sc, &sc->sc_rirb);

       return 0;
}

bool
hdaudio_resume(struct hdaudio_softc *sc)
{
       if (hdaudio_reset(sc) != 0)
               return false;

       hda_delay(HDAUDIO_CODEC_DELAY);

       /*
        * Ensure that the device is in a known state
        */
       hda_write2(sc, HDAUDIO_MMIO_STATESTS, HDAUDIO_STATESTS_SDIWAKE);
       hda_write1(sc, HDAUDIO_MMIO_RIRBSTS,
           HDAUDIO_RIRBSTS_RIRBOIS | HDAUDIO_RIRBSTS_RINTFL);
       hda_write4(sc, HDAUDIO_MMIO_INTSTS,
           hda_read4(sc, HDAUDIO_MMIO_INTSTS));
       hda_write4(sc, HDAUDIO_MMIO_DPLBASE, 0);
       hda_write4(sc, HDAUDIO_MMIO_DPUBASE, 0);

       if (hdaudio_corb_config(sc) != 0)
               return false;
       if (hdaudio_rirb_config(sc) != 0)
               return false;
       if (hdaudio_corb_start(sc) != 0)
               return false;
       if (hdaudio_rirb_start(sc) != 0)
               return false;

       hdaudio_intr_enable(sc);

       return true;
}

int
hdaudio_rescan(struct hdaudio_softc *sc, const char *ifattr, const int *locs)
{
       struct hdaudio_codec *co;
       struct hdaudio_function_group *fg;
       unsigned int codec;

       for (codec = 0; codec < HDAUDIO_MAX_CODECS; codec++) {
               co = &sc->sc_codec[codec];
               fg = co->co_fg;
               if (!co->co_valid || fg == NULL)
                       continue;
               if (fg->fg_device)
                       continue;
               hdaudio_attach_fg(fg, NULL);
       }

       return 0;
}

void
hdaudio_childdet(struct hdaudio_softc *sc, device_t child)
{
       struct hdaudio_codec *co;
       struct hdaudio_function_group *fg;
       unsigned int codec;

       for (codec = 0; codec < HDAUDIO_MAX_CODECS; codec++) {
               co = &sc->sc_codec[codec];
               fg = co->co_fg;
               if (!co->co_valid || fg == NULL)
                       continue;
               if (fg->fg_device == child)
                       fg->fg_device = NULL;
       }
}

int
hdaudio_intr(struct hdaudio_softc *sc)
{
       struct hdaudio_stream *st;
       uint32_t intsts, stream_mask;
       int streamid = 0;
       uint8_t rirbsts;

       intsts = hda_read4(sc, HDAUDIO_MMIO_INTSTS);
       if (!(intsts & HDAUDIO_INTSTS_GIS))
               return 0;

       if (intsts & HDAUDIO_INTSTS_CIS) {
               rirbsts = hda_read1(sc, HDAUDIO_MMIO_RIRBSTS);
               if (rirbsts & HDAUDIO_RIRBSTS_RINTFL) {
                       mutex_enter(&sc->sc_corb_mtx);
                       hdaudio_rirb_dequeue(sc, true);
                       mutex_exit(&sc->sc_corb_mtx);
               }
               if (rirbsts & (HDAUDIO_RIRBSTS_RIRBOIS|HDAUDIO_RIRBSTS_RINTFL))
                       hda_write1(sc, HDAUDIO_MMIO_RIRBSTS, rirbsts);
               hda_write4(sc, HDAUDIO_MMIO_INTSTS, HDAUDIO_INTSTS_CIS);
       }
       if (intsts & HDAUDIO_INTSTS_SIS_MASK) {
               mutex_enter(&sc->sc_stream_mtx);
               stream_mask = intsts & sc->sc_stream_mask;
               while (streamid < HDAUDIO_MAX_STREAMS && stream_mask != 0) {
                       st = &sc->sc_stream[streamid++];
                       if ((stream_mask & 1) != 0 && st->st_intr) {
                               st->st_intr(st);
                       }
                       stream_mask >>= 1;
               }
               mutex_exit(&sc->sc_stream_mtx);
               hda_write4(sc, HDAUDIO_MMIO_INTSTS, HDAUDIO_INTSTS_SIS_MASK);
       }

       return 1;
}

struct hdaudio_stream *
hdaudio_stream_establish(struct hdaudio_softc *sc,
   enum hdaudio_stream_type type, int (*intr)(struct hdaudio_stream *),
   void *cookie)
{
       struct hdaudio_stream *st;
       struct hdaudio_dma dma;
       int i, err;

       dma.dma_size = sizeof(struct hdaudio_bdl_entry) * HDAUDIO_BDL_MAX;
       dma.dma_sizereg = 0;
       err = hdaudio_dma_alloc(sc, &dma, BUS_DMA_COHERENT | BUS_DMA_NOCACHE);
       if (err)
               return NULL;

       mutex_enter(&sc->sc_stream_mtx);
       for (i = 0; i < HDAUDIO_MAX_STREAMS; i++) {
               st = &sc->sc_stream[i];
               if (st->st_enable == false)
                       break;
               if (st->st_type != type)
                       continue;
               if (sc->sc_stream_mask & (1 << i))
                       continue;

               /* Allocate stream */
               st->st_bdl = dma;
               st->st_intr = intr;
               st->st_cookie = cookie;
               sc->sc_stream_mask |= (1 << i);
               mutex_exit(&sc->sc_stream_mtx);
               return st;
       }
       mutex_exit(&sc->sc_stream_mtx);

       /* No streams of requested type available */
       hdaudio_dma_free(sc, &dma);
       return NULL;
}

void
hdaudio_stream_disestablish(struct hdaudio_stream *st)
{
       struct hdaudio_softc *sc = st->st_host;
       struct hdaudio_dma dma;

       KASSERT(sc->sc_stream_mask & (1 << st->st_shift));

       mutex_enter(&sc->sc_stream_mtx);
       sc->sc_stream_mask &= ~(1 << st->st_shift);
       st->st_intr = NULL;
       st->st_cookie = NULL;
       dma = st->st_bdl;
       st->st_bdl.dma_valid = false;
       mutex_exit(&sc->sc_stream_mtx);

       /* Can't bus_dmamem_unmap while holding a mutex.  */
       hdaudio_dma_free(sc, &dma);
}

/*
* Convert most of audio_params_t to stream fmt descriptor; noticeably missing
* is the # channels bits, as this is encoded differently in codec and
* stream descriptors.
*
* TODO: validate that the stream and selected codecs can handle the fmt
*/
uint16_t
hdaudio_stream_param(struct hdaudio_stream *st, const audio_params_t *param)
{
       uint16_t fmt = 0;

       switch (param->encoding) {
       case AUDIO_ENCODING_AC3:
               fmt |= HDAUDIO_FMT_TYPE_NONPCM;
               break;
       default:
               fmt |= HDAUDIO_FMT_TYPE_PCM;
               break;
       }

       switch (param->sample_rate) {
       case 8000:
               fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(1) |
                   HDAUDIO_FMT_DIV(6);
               break;
       case 11025:
               fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(1) |
                   HDAUDIO_FMT_DIV(4);
               break;
       case 16000:
               fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(1) |
                   HDAUDIO_FMT_DIV(3);
               break;
       case 22050:
               fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(1) |
                   HDAUDIO_FMT_DIV(2);
               break;
       case 32000:
               fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(2) |
                   HDAUDIO_FMT_DIV(3);
               break;
       case 44100:
               fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(1);
               break;
       case 48000:
               fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(1);
               break;
       case 88200:
               fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(2);
               break;
       case 96000:
               fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(2);
               break;
       case 176400:
               fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(4);
               break;
       case 192000:
               fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(4);
               break;
       default:
               return 0;
       }

       if (param->precision == 16 && param->validbits == 8)
               fmt |= HDAUDIO_FMT_BITS_8_16;
       else if (param->precision == 16 && param->validbits == 16)
               fmt |= HDAUDIO_FMT_BITS_16_16;
       else if (param->precision == 32 && param->validbits == 20)
               fmt |= HDAUDIO_FMT_BITS_20_32;
       else if (param->precision == 32 && param->validbits == 24)
               fmt |= HDAUDIO_FMT_BITS_24_32;
       else if (param->precision == 32 && param->validbits == 32)
               fmt |= HDAUDIO_FMT_BITS_32_32;
       else
               return 0;

       return fmt;
}

void
hdaudio_stream_reset(struct hdaudio_stream *st)
{
       struct hdaudio_softc *sc = st->st_host;
       int snum = st->st_shift;
       int retry;
       uint8_t ctl0;

       ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
       ctl0 |= HDAUDIO_CTL_SRST;
       hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);

       retry = HDAUDIO_RESET_TIMEOUT;
       do {
               ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
               if (ctl0 & HDAUDIO_CTL_SRST)
                       break;
               hda_delay(10);
       } while (--retry > 0);

       ctl0 &= ~HDAUDIO_CTL_SRST;
       hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);

       retry = HDAUDIO_RESET_TIMEOUT;
       do {
               ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
               if (!(ctl0 & HDAUDIO_CTL_SRST))
                       break;
               hda_delay(10);
       } while (--retry > 0);
       if (retry == 0) {
               hda_error(sc, "timeout leaving stream reset state\n");
               return;
       }
}

void
hdaudio_stream_start(struct hdaudio_stream *st, int blksize,
   bus_size_t dmasize, const audio_params_t *params)
{
       struct hdaudio_softc *sc = st->st_host;
       struct hdaudio_bdl_entry *bdl;
       uint64_t dmaaddr;
       uint32_t intctl;
       uint16_t fmt;
       uint8_t ctl0, ctl2;
       int cnt, snum = st->st_shift;

       KASSERT(sc->sc_stream_mask & (1 << st->st_shift));
       KASSERT(st->st_data.dma_valid == true);
       KASSERT(st->st_bdl.dma_valid == true);

       hdaudio_stream_stop(st);
       hdaudio_stream_reset(st);

       /*
        * Configure buffer descriptor list
        */
       dmaaddr = DMA_DMAADDR(&st->st_data);
       bdl = DMA_KERNADDR(&st->st_bdl);
       for (cnt = 0; cnt < HDAUDIO_BDL_MAX; cnt++) {
               bdl[cnt].address_lo = (uint32_t)dmaaddr;
               bdl[cnt].address_hi = dmaaddr >> 32;
               bdl[cnt].length = blksize;
               bdl[cnt].flags = HDAUDIO_BDL_ENTRY_IOC;
               dmaaddr += blksize;
               if (dmaaddr >= DMA_DMAADDR(&st->st_data) + dmasize) {
                       cnt++;
                       break;
               }
       }

       /*
        * Program buffer descriptor list
        */
       dmaaddr = DMA_DMAADDR(&st->st_bdl);
       hda_write4(sc, HDAUDIO_SD_BDPL(snum), (uint32_t)dmaaddr);
       hda_write4(sc, HDAUDIO_SD_BDPU(snum), (uint32_t)(dmaaddr >> 32));
       hda_write2(sc, HDAUDIO_SD_LVI(snum), (cnt - 1) & 0xff);

       /*
        * Program cyclic buffer length
        */
       hda_write4(sc, HDAUDIO_SD_CBL(snum), dmasize);

       /*
        * Program stream number (tag). Although controller hardware is
        * capable of transmitting any stream number (0-15), by convention
        * stream 0 is reserved as unused by software, so that converters
        * whose stream numbers have been reset to 0 do not unintentionally
        * decode data not intended for them.
        */
       ctl2 = hda_read1(sc, HDAUDIO_SD_CTL2(snum));
       ctl2 &= ~0xf0;
       ctl2 |= hdaudio_stream_tag(st) << 4;
       hda_write1(sc, HDAUDIO_SD_CTL2(snum), ctl2);

       /*
        * Program stream format
        */
       fmt = hdaudio_stream_param(st, params) |
           HDAUDIO_FMT_CHAN(params->channels);
       hda_write2(sc, HDAUDIO_SD_FMT(snum), fmt);

       /*
        * Switch on interrupts for this stream
        */
       intctl = hda_read4(sc, HDAUDIO_MMIO_INTCTL);
       intctl |= (1 << st->st_shift);
       hda_write4(sc, HDAUDIO_MMIO_INTCTL, intctl);

       /*
        * Start running the stream
        */
       ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
       ctl0 |= HDAUDIO_CTL_DEIE | HDAUDIO_CTL_FEIE | HDAUDIO_CTL_IOCE |
           HDAUDIO_CTL_RUN;
       hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);
}

void
hdaudio_stream_stop(struct hdaudio_stream *st)
{
       struct hdaudio_softc *sc = st->st_host;
       uint32_t intctl;
       uint8_t ctl0;
       int snum = st->st_shift;

       /*
        * Stop running the stream
        */
       ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
       ctl0 &= ~(HDAUDIO_CTL_DEIE | HDAUDIO_CTL_FEIE | HDAUDIO_CTL_IOCE |
           HDAUDIO_CTL_RUN);
       hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);

       /*
        * Switch off interrupts for this stream
        */
       intctl = hda_read4(sc, HDAUDIO_MMIO_INTCTL);
       intctl &= ~(1 << st->st_shift);
       hda_write4(sc, HDAUDIO_MMIO_INTCTL, intctl);
}

/*
* /dev/hdaudioN interface
*/

static const char *
hdaudioioctl_fgrp_to_cstr(enum function_group_type type)
{
       switch (type) {
       case HDAUDIO_GROUP_TYPE_AFG:
               return "afg";
       case HDAUDIO_GROUP_TYPE_VSM_FG:
               return "vsmfg";
       default:
               return "unknown";
       }
}

static struct hdaudio_function_group *
hdaudioioctl_fgrp_lookup(struct hdaudio_softc *sc, int codecid, int nid)
{
       struct hdaudio_codec *co;
       struct hdaudio_function_group *fg = NULL;
       int i;

       if (codecid < 0 || codecid >= HDAUDIO_MAX_CODECS)
               return NULL;
       co = &sc->sc_codec[codecid];
       if (co->co_valid == false)
               return NULL;

       for (i = 0; i < co->co_nfg; i++)
               if (co->co_fg[i].fg_nid == nid) {
                       fg = &co->co_fg[i];
                       break;
               }

       return fg;
}

static int
hdaudioioctl_fgrp_info(struct hdaudio_softc *sc, prop_dictionary_t request,
   prop_dictionary_t response)
{
       struct hdaudio_codec *co;
       struct hdaudio_function_group *fg;
       prop_array_t array;
       prop_dictionary_t dict;
       int codecid, fgid;

       array = prop_array_create();
       if (array == NULL)
               return ENOMEM;

       for (codecid = 0; codecid < HDAUDIO_MAX_CODECS; codecid++) {
               co = &sc->sc_codec[codecid];
               if (co->co_valid == false)
                       continue;
               for (fgid = 0; fgid < co->co_nfg; fgid++) {
                       fg = &co->co_fg[fgid];
                       dict = prop_dictionary_create();
                       if (dict == NULL)
                               return ENOMEM;
                       prop_dictionary_set_string_nocopy(dict,
                           "type", hdaudioioctl_fgrp_to_cstr(fg->fg_type));
                       prop_dictionary_set_int16(dict, "nid", fg->fg_nid);
                       prop_dictionary_set_int16(dict, "codecid", codecid);
                       prop_dictionary_set_uint16(dict, "vendor-id",
                           fg->fg_vendor);
                       prop_dictionary_set_uint16(dict, "product-id",
                           fg->fg_product);
                       prop_dictionary_set_uint32(dict, "subsystem-id",
                           sc->sc_subsystem);
                       if (fg->fg_device)
                               prop_dictionary_set_string(dict, "device",
                                   device_xname(fg->fg_device));
                       else
                               prop_dictionary_set_string_nocopy(dict,
                                   "device", "<none>");
                       prop_array_add(array, dict);
               }
       }

       prop_dictionary_set(response, "function-group-info", array);
       return 0;
}

static int
hdaudioioctl_fgrp_getconfig(struct hdaudio_softc *sc,
   prop_dictionary_t request, prop_dictionary_t response)
{
       struct hdaudio_function_group *fg;
       prop_dictionary_t dict;
       prop_array_t array;
       uint32_t nodecnt, wcap, config;
       int16_t codecid, nid, i;
       int startnode, endnode;

       if (!prop_dictionary_get_int16(request, "codecid", &codecid) ||
           !prop_dictionary_get_int16(request, "nid", &nid))
               return EINVAL;

       fg = hdaudioioctl_fgrp_lookup(sc, codecid, nid);
       if (fg == NULL)
               return ENODEV;

       array = prop_array_create();
       if (array == NULL)
               return ENOMEM;

       nodecnt = hdaudio_command(fg->fg_codec, fg->fg_nid,
           CORB_GET_PARAMETER, COP_SUBORDINATE_NODE_COUNT);
       startnode = COP_NODECNT_STARTNODE(nodecnt);
       endnode = startnode + COP_NODECNT_NUMNODES(nodecnt);

       for (i = startnode; i < endnode; i++) {
               wcap = hdaudio_command(fg->fg_codec, i,
                   CORB_GET_PARAMETER, COP_AUDIO_WIDGET_CAPABILITIES);
               if (COP_AWCAP_TYPE(wcap) != COP_AWCAP_TYPE_PIN_COMPLEX)
                       continue;
               config = hdaudio_command(fg->fg_codec, i,
                   CORB_GET_CONFIGURATION_DEFAULT, 0);
               dict = prop_dictionary_create();
               if (dict == NULL)
                       return ENOMEM;
               prop_dictionary_set_int16(dict, "nid", i);
               prop_dictionary_set_uint32(dict, "config", config);
               prop_array_add(array, dict);
       }

       prop_dictionary_set(response, "pin-config", array);

       return 0;
}

static int
hdaudioioctl_fgrp_setconfig(struct hdaudio_softc *sc,
   prop_dictionary_t request, prop_dictionary_t response)
{
       struct hdaudio_function_group *fg;
       prop_array_t config;
       int16_t codecid, nid;
       int err;

       if (!prop_dictionary_get_int16(request, "codecid", &codecid) ||
           !prop_dictionary_get_int16(request, "nid", &nid))
               return EINVAL;

       fg = hdaudioioctl_fgrp_lookup(sc, codecid, nid);
       if (fg == NULL)
               return ENODEV;

       if (fg->fg_device) {
               err = config_detach(fg->fg_device, 0);
               if (err)
                       return err;
               fg->fg_device = NULL;
       }

       /* "pin-config" may be NULL, this means "use BIOS configuration" */
       config = prop_dictionary_get(request, "pin-config");
       if (config && prop_object_type(config) != PROP_TYPE_ARRAY) {
               prop_object_release(config);
               return EINVAL;
       }
       hdaudio_attach_fg(fg, config);
       if (config)
               prop_object_release(config);

       return 0;
}

static int
hdaudio_dispatch_fgrp_ioctl(struct hdaudio_softc *sc, u_long cmd,
   prop_dictionary_t request, prop_dictionary_t response)
{
       struct hdaudio_function_group *fg;
       int (*infocb)(void *, prop_dictionary_t, prop_dictionary_t);
       prop_dictionary_t fgrp_dict;
       uint64_t info_fn;
       int16_t codecid, nid;
       void *fgrp_sc;
       bool rv;
       int err;

       if (!prop_dictionary_get_int16(request, "codecid", &codecid) ||
           !prop_dictionary_get_int16(request, "nid", &nid))
               return EINVAL;

       fg = hdaudioioctl_fgrp_lookup(sc, codecid, nid);
       if (fg == NULL)
               return ENODEV;
       if (fg->fg_device == NULL)
               return ENXIO;
       fgrp_sc = device_private(fg->fg_device);
       fgrp_dict = device_properties(fg->fg_device);

       switch (fg->fg_type) {
       case HDAUDIO_GROUP_TYPE_AFG:
               switch (cmd) {
               case HDAUDIO_FGRP_CODEC_INFO:
                       rv = prop_dictionary_get_uint64(fgrp_dict,
                           "codecinfo-callback", &info_fn);
                       if (!rv)
                               return ENXIO;
                       infocb = (void *)(uintptr_t)info_fn;
                       err = infocb(fgrp_sc, request, response);
                       break;
               case HDAUDIO_FGRP_WIDGET_INFO:
                       rv = prop_dictionary_get_uint64(fgrp_dict,
                           "widgetinfo-callback", &info_fn);
                       if (!rv)
                               return ENXIO;
                       infocb = (void *)(uintptr_t)info_fn;
                       err = infocb(fgrp_sc, request, response);
                       break;
               default:
                       err = EINVAL;
                       break;
               }
               break;

       default:
               err = EINVAL;
               break;
       }
       return err;
}

int
hdaudioopen(dev_t dev, int flag, int mode, struct lwp *l)
{
       device_t self;

       self = device_lookup(&hdaudio_cd, HDAUDIOUNIT(dev));
       if (self == NULL)
               return ENXIO;

       return 0;
}

int
hdaudioclose(dev_t dev, int flag, int mode, struct lwp *l)
{
       return 0;
}

int
hdaudioioctl(dev_t dev, u_long cmd, void *addr, int flag, struct lwp *l)
{
       struct hdaudio_softc *sc;
       struct plistref *pref = addr;
       prop_dictionary_t request, response;
       int err;

       sc = device_lookup_private(&hdaudio_cd, HDAUDIOUNIT(dev));
       if (sc == NULL)
               return ENXIO;

       response = prop_dictionary_create();
       if (response == NULL)
               return ENOMEM;

       err = prop_dictionary_copyin_ioctl(pref, cmd, &request);
       if (err) {
               prop_object_release(response);
               return err;
       }

       switch (cmd) {
       case HDAUDIO_FGRP_INFO:
               err = hdaudioioctl_fgrp_info(sc, request, response);
               break;
       case HDAUDIO_FGRP_GETCONFIG:
               err = hdaudioioctl_fgrp_getconfig(sc, request, response);
               break;
       case HDAUDIO_FGRP_SETCONFIG:
               err = hdaudioioctl_fgrp_setconfig(sc, request, response);
               break;
       case HDAUDIO_FGRP_CODEC_INFO:
       case HDAUDIO_FGRP_WIDGET_INFO:
               err = hdaudio_dispatch_fgrp_ioctl(sc, cmd, request, response);
               break;
       default:
               err = EINVAL;
               break;
       }

       if (!err)
               err = prop_dictionary_copyout_ioctl(pref, cmd, response);

       if (response)
               prop_object_release(response);
       prop_object_release(request);
       return err;
}

MODULE(MODULE_CLASS_DRIVER, hdaudio, "audio");
#ifdef _MODULE
static const struct cfiattrdata hdaudiobuscf_iattrdata = {
       "hdaudiobus", 1, {
               { "nid", "-1", -1 },
       }
};
static const struct cfiattrdata * const hdaudio_attrs[] = {
       &hdaudiobuscf_iattrdata, NULL
};
CFDRIVER_DECL(hdaudio, DV_AUDIODEV, hdaudio_attrs);
#endif

static int
hdaudio_modcmd(modcmd_t cmd, void *opaque)
{
       int error = 0;
#ifdef _MODULE
       int bmaj = -1, cmaj = -1;
#endif

       switch (cmd) {
       case MODULE_CMD_INIT:
#ifdef _MODULE
               error = devsw_attach("hdaudio", NULL, &bmaj,
                   &hdaudio_cdevsw, &cmaj);
               if (error)
                       break;
               error = config_cfdriver_attach(&hdaudio_cd);
               if (error)
                       devsw_detach(NULL, &hdaudio_cdevsw);
#endif
               break;
       case MODULE_CMD_FINI:
#ifdef _MODULE
               error = config_cfdriver_detach(&hdaudio_cd);
               if (error)
                       break;
               devsw_detach(NULL, &hdaudio_cdevsw);
#endif
               break;
       default:
               error = ENOTTY;
               break;
       }
       return error;
}

DEV_VERBOSE_DEFINE(hdaudio);