/* _NVRM_COPYRIGHT_BEGIN_
 *
 * Copyright 2001-2002 by NVIDIA Corporation.  All rights reserved.  All
 * information contained herein is proprietary and confidential to NVIDIA
 * Corporation.  Any use, reproduction, or disclosure without the written
 * permission of NVIDIA Corporation is prohibited.
 *
 * _NVRM_COPYRIGHT_END_
 */

#include "nv-misc.h"
#include "os-interface.h"
#include "nv.h"
#include "nv-freebsd.h"

#if __FreeBSD_version >= 700055
static void nvidia_pci_identify (driver_t *, device_t);
#endif
static int  nvidia_pci_probe    (device_t);
static int  nvidia_pci_attach   (device_t);
static int  nvidia_pci_detach   (device_t);

static void nvidia_pci_disable_serr  (device_t dev);
static void nvidia_pci_enable_intx   (device_t dev);

#if __FreeBSD_version >= 700055
void nvidia_pci_identify(driver_t *driver, device_t parent)
{
    if (device_find_child(parent, "nvidia", -1) == NULL)
        device_add_child(parent, "nvidia", -1);
}
#endif

int nvidia_pci_probe(device_t dev)
{
    U016 device;
    char name[NV_DEVICE_NAME_LENGTH];
    nv_stack_t *sp;

    if ((pci_get_class(dev) != PCIC_DISPLAY) ||
            ((pci_get_subclass(dev) != PCIS_DISPLAY_VGA) &&
             (pci_get_subclass(dev) != PCIS_DISPLAY_3D)) ||
            (pci_get_vendor(dev) != 0x10de) ||
            ((device = pci_get_device(dev)) < 0x0020))
        return ENXIO;

    NV_UMA_ZONE_ALLOC_STACK(sp);
    if (sp == NULL)
        return ENOMEM;

    if (rm_is_legacy_device(sp, device, TRUE)) {
        NV_UMA_ZONE_FREE_STACK(sp);
        return ENXIO;
    }

    if (rm_get_device_name(sp, NULL, device, NV_DEVICE_NAME_LENGTH, name)
            != RM_OK) {
        strcpy(name, "Unknown");
    }

    NV_UMA_ZONE_FREE_STACK(sp);

    device_set_desc_copy(dev, name);

    return 0;
}

int nvidia_pci_setup_intr(device_t dev)
{
    int status, flags;
    struct nvidia_softc *sc;

    sc = device_get_softc(dev);

    /* XXX Revisit! (INTR_FAST, INTR_MPSAFE) */
    flags = INTR_TYPE_AV;

#if __FreeBSD_version >= 700031
    status = bus_setup_intr(dev, sc->irq, flags, NULL, nvidia_intr, sc, &sc->irq_ih);
#else
    status = bus_setup_intr(dev, sc->irq, flags, nvidia_intr, sc, &sc->irq_ih);
#endif
    if (status) {
        device_printf(dev, "NVRM: HW ISR setup failed.\n");
        goto fail;
    }

fail:
    return status;
}

int nvidia_pci_teardown_intr(device_t dev)
{
    int status;
    struct nvidia_softc *sc;

    sc = device_get_softc(dev);

    status = bus_teardown_intr(dev, sc->irq, sc->irq_ih);
    if (status) {
        device_printf(dev, "NVRM: HW ISR teardown failed.\n");
        goto fail;
    }

fail:
    return status;
}

static void nvidia_pci_disable_serr(device_t dev)
{
    U016 word;
    word = (os_pci_read_word(dev, PCIR_COMMAND) & ~PCIM_CMD_SERRESPEN);
    os_pci_write_word(dev, PCIR_COMMAND, word);
}

static void nvidia_pci_enable_intx(device_t dev)
{
    U016 word;
    word = (os_pci_read_word(dev, PCIR_COMMAND) & ~PCIM_CMD_INTXDIS);
    os_pci_write_word(dev, PCIR_COMMAND, word);
}

void nvidia_pci_check_config_space(
    nv_stack_t *sp,
    device_t dev,
    BOOL check_the_bars,
    BOOL acquire_semaphore,
    BOOL may_block
)
{
    U032 BAR_low, BAR_high;
    U016 word, i;
    struct nvidia_softc *sc = device_get_softc(dev);
    nv_state_t *nv = sc->nv_state;

    if (nv->flags & NV_FLAG_USE_BAR0_CFG) {
        rm_check_pci_config_space(sp, nv, check_the_bars,
            acquire_semaphore, may_block);
        return;
    }

    word = os_pci_read_word(dev, PCIR_COMMAND);

    if ((word & PCIM_CMD_BUSMASTEREN) == 0)
        pci_enable_busmaster(dev);

    if ((word & PCIM_CMD_MEMEN) == 0)
        pci_enable_io(dev, SYS_RES_MEMORY);

    if ((word & PCIM_CMD_SERRESPEN) != 0)
        nvidia_pci_disable_serr(dev);

    if ((word & PCIM_CMD_INTXDIS) != 0)
        nvidia_pci_enable_intx(dev);

    if (check_the_bars)
    {
        for (i = 0; i < NV_GPU_NUM_BARS; i++) {
            BAR_low = BAR_high = 0;
            nv_aperture_t *BAR = &nv->bars[i];
            if (BAR->offset == 0) continue;

            BAR_low = os_pci_read_dword(dev, BAR->offset);
            if ((BAR_low & NVRM_PCICFG_BAR_ADDR_MASK) != BAR->address)
                os_pci_write_dword(dev, BAR->offset, BAR->address);

            if ((BAR_low & NVRM_PCICFG_BAR_MEMTYPE_MASK)
                    != NVRM_PCICFG_BAR_MEMTYPE_64BIT)
                continue;

            BAR_high = os_pci_read_dword(dev, BAR->offset + 4);
            if ((BAR_low & NVRM_PCICFG_BAR_ADDR_MASK) == BAR->address
                    && BAR_high == 0)
                continue;

            os_pci_write_dword(dev, BAR->offset, BAR->address); /* 31:4 */
            os_pci_write_dword(dev, BAR->offset + 4, 0); /* 63:32 */
        }
    }
}

U008 nvidia_pci_find_capability(device_t dev, U008 capability)
{
    U016 status;
    U008 cap_ptr, cap_id;

    status = pci_read_config(dev, PCIR_STATUS, 2);
    status &= PCIM_STATUS_CAPPRESENT;
    if (!status)
        goto failed;

    switch (pci_get_class(dev)) {
        case PCIC_DISPLAY:
        case PCIC_BRIDGE:
            cap_ptr = pci_read_config(dev, PCIR_CAP_PTR, 1);
            break;
        default:
            goto failed;
    }

    do {
        cap_ptr &= 0xfc;
        cap_id = pci_read_config(dev, cap_ptr + PCIR_CAP_LIST_ID, 1);
        if (cap_id == capability) {
            return cap_ptr;
        }
        cap_ptr = pci_read_config(dev, cap_ptr + PCIR_CAP_LIST_NEXT, 1);
    } while (cap_ptr && cap_id != 0xff);

failed:
    return 0;
}

int nvidia_pci_attach(device_t dev)
{
    int status;
    struct nvidia_softc *sc;
    U016 word, i, j;
    U032 BAR_low, req;
    nv_stack_t *sp;

    if (device_get_unit(dev) >= NV_MAX_DEVICES) {
        device_printf(dev, "NVRM: maximum device number exceeded.\n");
        return ENXIO;
    }

    sc = device_get_softc(dev); /* first reference */

    bzero(sc, sizeof(nvidia_softc_t));
    STAILQ_INIT(&sc->filep_queue);

    sc->nv_state = malloc(sizeof(nv_state_t), M_NVIDIA, M_WAITOK | M_ZERO);
    if (sc->nv_state == NULL)
        return ENOMEM;

    pci_enable_busmaster(dev);
    word = pci_read_config(dev, PCIR_COMMAND, 2);

    if ((word & PCIM_CMD_BUSMASTEREN) == 0) {
        device_printf(dev, "NVRM: PCI busmaster enable failed.\n");
        return ENXIO;
    }

    pci_enable_io(dev, SYS_RES_MEMORY);
    word = pci_read_config(dev, PCIR_COMMAND, 2);

    if ((word & PCIM_CMD_MEMEN) == 0) {
        device_printf(dev, "NVRM: PCI memory enable failed.\n");
        return ENXIO;
    }

    for (i = 0, j = 0; i < NVRM_PCICFG_NUM_BARS && j < NV_GPU_NUM_BARS; i++) {
        U008 offset = NVRM_PCICFG_BAR_OFFSET(i);
        BAR_low = os_pci_read_dword(dev, offset);
        os_pci_write_dword(dev, offset, 0xffffffff);
        req = os_pci_read_dword(dev, offset);
        if ((req != 0) /* implemented */ && (req & NVRM_PCICFG_BAR_REQTYPE_MASK)
                == NVRM_PCICFG_BAR_REQTYPE_MEMORY) {
            sc->nv_state->bars[j].offset = offset;
            sc->BAR_rids[j] = offset; j++;
            if ((req & NVRM_PCICFG_BAR_MEMTYPE_MASK) == NVRM_PCICFG_BAR_MEMTYPE_64BIT)
                i++;
        }
        os_pci_write_dword(dev, offset, BAR_low);
    }

    sc->irq_rid = 0;
    sc->iop_rid = 0;

    NV_UMA_ZONE_ALLOC_STACK(sp);
    if (sp == NULL) {
        free(sc->nv_state, M_NVIDIA);
        return ENOMEM;
    }

    sc->attach_sp = sp;

    status = nvidia_alloc_hardware(dev);
    if (status) {
        device_printf(dev, "NVRM: NVIDIA hardware alloc failed.\n");
        goto fail;
    }

    status = nvidia_attach(dev);
    if (status) {
        device_printf(dev, "NVRM: NVIDIA driver attach failed.\n");
        goto fail;
    }

    status = nvidia_pci_setup_intr(dev);
    if (status) {
        device_printf(dev, "NVRM: NVIDIA driver interrupt setup failed.\n");
        nvidia_detach(dev);
        goto fail;
    }

    if (!rm_init_private_state(sp, sc->nv_state)) {
        nvidia_pci_teardown_intr(dev);
        device_printf(dev, "NVRM: rm_init_private_state() failed.\n");
        nvidia_detach(dev);
        goto fail;
    }

    mtx_init(&sc->rm_mtx, "dev.rm_mtx", NULL, MTX_SPIN | MTX_RECURSE);
    sx_init(&sc->api_sx, "dev.api_sx");

    return 0;

fail:
    nvidia_free_hardware(dev);

    sc->attach_sp = NULL;
    NV_UMA_ZONE_FREE_STACK(sp);

    free(sc->nv_state, M_NVIDIA);

    return status;
}

int nvidia_pci_detach(device_t dev)
{
    int status;
    nv_stack_t *sp;
    struct nvidia_softc *sc;
    nv_state_t *nv;

    /*
     * Check if the device is still in use before accepting the
     * detach request; this event can happen even when the module
     * usage count is non-zero!
     */
    sc = device_get_softc(dev);
    nv = sc->nv_state;

    nv_lock_api(nv);

    if (sc->refcnt != 0) { /* XXX Fix me? (refcnt) */
        nv_unlock_api(nv);
        return EBUSY;
    }

    nv_unlock_api(nv);

    mtx_destroy(&sc->rm_mtx);
    sx_destroy(&sc->api_sx);

    status = nvidia_pci_teardown_intr(dev);
    if (status)
        goto fail;

    status = nvidia_detach(dev);
    if (status) {
        device_printf(dev, "NVRM: NVIDIA driver detach failed.\n");
        goto fail;
    }

    sp = sc->attach_sp;

    rm_free_private_state(sp, sc->nv_state);
    nvidia_free_hardware(dev);

    sc->attach_sp = NULL;
    NV_UMA_ZONE_FREE_STACK(sp);

    free(sc->nv_state, M_NVIDIA);

fail:
    /* XXX Fix me? (state) */
    return status;
}

static device_method_t nvidia_pci_methods[] = {
#if __FreeBSD_version >= 700055
    DEVMETHOD( device_identify, nvidia_pci_identify ),
#endif
    DEVMETHOD( device_probe,    nvidia_pci_probe    ),
    DEVMETHOD( device_attach,   nvidia_pci_attach   ),
    DEVMETHOD( device_detach,   nvidia_pci_detach   ),
#ifdef NV_SUPPORT_ACPI_PM
    DEVMETHOD( device_suspend,  nvidia_suspend      ),
    DEVMETHOD( device_resume,   nvidia_resume       ),
#endif
    { 0, 0 }
};

static driver_t nvidia_pci_driver = {
    "nvidia",
    nvidia_pci_methods,
    sizeof(struct nvidia_softc)
};

#if __FreeBSD_version >= 700055
DRIVER_MODULE(nvidia, vgapci, nvidia_pci_driver, nvidia_devclass, nvidia_modevent, 0);
#else
DRIVER_MODULE(nvidia, pci, nvidia_pci_driver, nvidia_devclass, nvidia_modevent, 0);
#endif

MODULE_DEPEND(nvidia, mem, 1, 1, 1);
MODULE_DEPEND(nvidia, io, 1, 1, 1);

#ifdef NV_SUPPORT_OS_AGP
MODULE_DEPEND(nvidia, agp, 1, 1, 1);
#endif

#ifdef NV_SUPPORT_LINUX_COMPAT /* (COMPAT_LINUX || COMPAT_LINUX32) */
MODULE_DEPEND(nvidia, linux, 1, 1, 1);
#endif
