/* _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"
#include "nv-reg.h"

struct sysctl_ctx_list sysctl_ctx;

struct sysctl_oid *oid_nvidia;
struct sysctl_oid *oid_agp;
struct sysctl_oid *oid_registry;

static char *option_string = NULL;

static void*  nvidia_find_bridge     (void);
static NvU32  nvidia_dev_agp_cmd     (device_t dev);
static NvU32  nvidia_dev_agp_status  (device_t dev);
static NvU32  nvidia_sys_agp_cmd     (device_t dev);

void nvidia_sysctl_init(void)
{
    device_t dev;
    struct sysctl_oid *oid;
    nv_parm_t *entry;

    sysctl_ctx_init(&sysctl_ctx);

    oid_nvidia = SYSCTL_ADD_NODE(&sysctl_ctx,
            SYSCTL_STATIC_CHILDREN(_hw),
            OID_AUTO,
            "nvidia",
            CTLFLAG_RD | CTLFLAG_DYN,
            0,
            "NVIDIA SYSCTL Master Node");

    oid_agp = SYSCTL_ADD_NODE(&sysctl_ctx,
            SYSCTL_CHILDREN(oid_nvidia),
            OID_AUTO,
            "agp",
            CTLFLAG_RD | CTLFLAG_DYN,
            0,
            "NVIDIA SYSCTL AGP Node");

    SYSCTL_ADD_STRING(&sysctl_ctx,
            SYSCTL_CHILDREN(oid_nvidia),
            OID_AUTO,
            "version",
            CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
            (char *)(uintptr_t) pNVRM_ID,
            0,
            "NVIDIA Resource Manager (NVRM) Version");

    oid_registry = SYSCTL_ADD_NODE(&sysctl_ctx,
            SYSCTL_CHILDREN(oid_nvidia),
            OID_AUTO,
            "registry",
            CTLFLAG_RD | CTLFLAG_DYN,
            0,
            "NVIDIA SYSCTL Registry Node");

    entry = nv_parms;
    do {
        SYSCTL_ADD_PROC(&sysctl_ctx,
            SYSCTL_CHILDREN(oid_registry),
            OID_AUTO,
            entry->name,
            CTLTYPE_UINT | CTLFLAG_RW | CTLFLAG_DYN,
            entry->data, 0,
            nvidia_sysctl_registry_key,
            "IU", NULL);
        entry++;
    } while(entry->name != NULL);

    option_string = malloc(1, M_NVIDIA, (M_WAITOK | M_ZERO));

    SYSCTL_ADD_PROC(&sysctl_ctx,
            SYSCTL_CHILDREN(oid_registry),
            OID_AUTO, "dwords",
            CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_DYN,
            NULL, 0,
            nvidia_sysctl_registry_dwords,
            "A", NULL);

    if ((dev = nvidia_find_bridge()) != NULL) {
        /*
         * Assume this is the only AGP capable bridge in the system
         * and report its capabilities (hw.nvidia.agp.bridge).
         */
        oid = SYSCTL_ADD_NODE(&sysctl_ctx,
                SYSCTL_CHILDREN(oid_agp),
                OID_AUTO,
                "host-bridge",
                CTLFLAG_RD | CTLFLAG_DYN,
                0,
                "NVIDIA AGP Bridge Node");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "rates",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) dev, 0,
                nvidia_sysctl_agp_rates,
                "A",
                "NVIDIA AGP Bridge Rates Info");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "fw",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) dev, 0,
                nvidia_sysctl_agp_fw,
                "A",
                "NVIDIA AGP Bridge FW Info");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "sba",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) dev, 0,
                nvidia_sysctl_agp_sba,
                "A",
                "NVIDIA AGP Bridge SBA Info");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "registers",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) dev, 0,
                nvidia_sysctl_agp_registers,
                "A",
                "NVIDIA AGP Bridge Registers");
    }
}

void nvidia_sysctl_exit(void)
{
    sysctl_ctx_free(&sysctl_ctx);
    if (option_string != NULL)
        free((void *)option_string, M_NVIDIA);
}


static NvU32 nvidia_dev_agp_cmd(device_t dev)
{
    NvU8  cap_ptr;
    NvU32 tmp;

    cap_ptr = nvidia_pci_find_capability(dev, PCIR_CAP_ID_AGP);
    tmp = pci_read_config(dev, cap_ptr + 8, 4);

    return tmp;
}

static NvU32 nvidia_dev_agp_status(device_t dev)
{
    NvU8  cap_ptr;
    NvU32 tmp;

    cap_ptr = nvidia_pci_find_capability(dev, PCIR_CAP_ID_AGP);
    tmp = pci_read_config(dev, cap_ptr + 4, 4);

    return tmp;
}

static NvU32 nvidia_sys_agp_cmd(device_t dev)
{
    NvU32 tmp;

    tmp = nvidia_dev_agp_cmd(dev);
    tmp &= nvidia_dev_agp_cmd(nvidia_find_bridge());

    return tmp;
}

static void* nvidia_find_bridge(void)
{
    NvU8  hdrtype;
    NvU32 bus;
    NvU32 device;
    NvU32 func;

    device_t dev;

    for (bus = 0; bus < 5; bus++) {
        for (device = 0; device < 32; device++) {
            for (func = 0; func < 8; func++) {
                /*
                 * We're not interested in non-bridge PCI devices, nor do
                 * care for devices with our own or an invalid vendor ID.
                 */
                dev = pci_find_bsf(bus, device, func);

                if (dev == NULL)
                {
                    if (func == 0) {
                        /*
                         * If a read to function zero of a specified
                         * bus/device master aborts, then it is assumed
                         * that no such device exists on the bus since
                         * devices are required to implement function
                         * number zero.
                         * In this case reads to the remaining functions
                         * are not necessary.
                         */
                        break;
                    } else
                        continue;
                }

                if (pci_get_class(dev) != PCIC_BRIDGE)
                    break;

                if (pci_get_vendor(dev) == 0xffff)
                    break;

                if (nvidia_pci_find_capability(dev, PCIR_CAP_ID_AGP) != 0)
                    return dev;

                /*
                 * It also doesn't make sense to to iterate over multiple
                 * functions if this isn't a multi-function device.
                 */
                hdrtype = pci_read_config(dev, PCIR_HDRTYPE, 1);

                if ((hdrtype & PCIM_MFDEV) == 0)
                    break;
            }
        }
    }

    return NULL;
}


int nvidia_sysctl_gpu_model(SYSCTL_HANDLER_ARGS)
{
    nv_state_t *nv = arg1;
    device_t dev = nv->handle;
    nv_stack_t *sp;
    char model_name[NV_DEVICE_NAME_LENGTH+1];
    NvU16 subvendor, subdevice;

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

    subvendor = pci_get_subvendor(dev);
    subdevice = pci_get_subdevice(dev);

    if (rm_get_device_name(sp, nv, nv->device_id, subvendor, subdevice,
                NV_DEVICE_NAME_LENGTH, model_name) != RM_OK) {
        strcpy(model_name, "Unknown");
    }

    NV_UMA_ZONE_FREE_STACK(sp);

    return SYSCTL_OUT(req, model_name, strlen(model_name) + 1);
}

int nvidia_sysctl_gpu_uuid(SYSCTL_HANDLER_ARGS)
{
    nv_state_t *nv = arg1;
    nv_stack_t *sp;
    NvU8 *uuid;
    NvU32 uuid_strlen;
    int ret;

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

    if (rm_get_gpu_uuid(sp, nv, &uuid, &uuid_strlen) != RM_OK)
        return EIO;

    NV_UMA_ZONE_FREE_STACK(sp);

    ret = SYSCTL_OUT(req, (char *)uuid, uuid_strlen);
    os_free_mem(uuid);

    return ret;
}

int nvidia_sysctl_gpu_vbios(SYSCTL_HANDLER_ARGS)
{
    nv_state_t *nv = arg1;
    nv_stack_t *sp;

    NvU32 vbios[5];
    NvU8  vbios_version[16];

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

    if (rm_get_vbios_version(sp, nv, &vbios[0], &vbios[1], &vbios[2],
                &vbios[3], &vbios[4]) != RM_OK) {
        /*
         * The VBIOS version is only accessible after the device has been
         * initialized with rm_init_adapter.
         */
        sprintf(vbios_version, "??.??.??.??.??");
    } else {
        sprintf(vbios_version, "%02x.%02x.%02x.%02x.%02x", vbios[0],
                vbios[1], vbios[2], vbios[3], vbios[4]);
    }

    NV_UMA_ZONE_FREE_STACK(sp);

    return SYSCTL_OUT(req, vbios_version, strlen(vbios_version) + 1);
}

int nvidia_sysctl_bus_type(SYSCTL_HANDLER_ARGS)
{
    struct nvidia_softc *sc = arg1;
    char *bus_type;

    if (nvidia_pci_find_capability(sc->dev, PCIR_CAP_ID_AGP) != 0)
        bus_type = "AGP";
    else if (nvidia_pci_find_capability(sc->dev, PCIR_CAP_ID_EXP) != 0)
        bus_type = "PCIe";
    else
        bus_type = "PCI";

    return SYSCTL_OUT(req, bus_type, strlen(bus_type) + 1);
}

int nvidia_sysctl_registry_key(SYSCTL_HANDLER_ARGS)
{
    int error;

    error = sysctl_handle_int(oidp, arg1, 0, req);

    if (error || !req->newptr)
        return error;

    /* refresh the registry with the updated option table */
    os_registry_init();

    return 0;
}

int nvidia_sysctl_registry_dwords(SYSCTL_HANDLER_ARGS)
{
    int error, len;
    char *new_option_string;

    len = strlen(option_string) + 1;
    error = SYSCTL_OUT(req, option_string, len);

    if (error || !req->newptr)
        return error;

    len = (req->newlen - req->newidx);

    new_option_string = malloc((len + 1), M_NVIDIA, M_WAITOK);
    if (!new_option_string)
        return ENOMEM;

    error = SYSCTL_IN(req, new_option_string, len);
    if (error)
        return error;

    free(option_string, M_NVIDIA);

    option_string = new_option_string;
    option_string[len] = '\0';

    nvidia_update_registry(new_option_string);

    return 0;
}

int nvidia_sysctl_agp_rates(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_rates[16];

    NvU32 tmp = nvidia_dev_agp_status(dev);

    if ((tmp & 0x08) != 0)
        tmp = (tmp & 0x07) << 2;

    sprintf(agp_rates, "%s%s%s%s",
            (tmp & 0x0008) ? "8x " : "",
            (tmp & 0x0004) ? "4x " : "",
            (tmp & 0x0002) ? "2x " : "",
            (tmp & 0x0001) ? "1x " : "");

    return SYSCTL_OUT(req, agp_rates, strlen(agp_rates) + 1);
}

int nvidia_sysctl_agp_fw(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_fw[16];

    sprintf(agp_fw, "%ssupported",
        ((nvidia_dev_agp_status(dev) & 0x10) == 0x10) ? "" : "not ");

    return SYSCTL_OUT(req, agp_fw, strlen(agp_fw) + 1);
}

int nvidia_sysctl_agp_sba(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_sba[16];

    sprintf(agp_sba, "%ssupported",
        ((nvidia_dev_agp_status(dev) & 0x200) == 0x200) ? "" : "not ");

    return SYSCTL_OUT(req, agp_sba, strlen(agp_sba) + 1);
}

int nvidia_sysctl_agp_registers(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_registers[24];

    sprintf(agp_registers, "0x%08x:0x%08x",
        nvidia_dev_agp_status(dev), nvidia_dev_agp_cmd(dev));

    return SYSCTL_OUT(req, agp_registers, strlen(agp_registers) + 1);
}

int nvidia_sysctl_agp_driver(SYSCTL_HANDLER_ARGS)
{
    nv_state_t *nv = arg1;
    NvU8 agp_driver[24];

    switch (nv->agp_config) {
        case NVOS_AGP_CONFIG_DISABLE_AGP:
            sprintf(agp_driver, "n/a (unused)");
            break;
        case NVOS_AGP_CONFIG_OSAGP:
            sprintf(agp_driver, "freebsd (agp.ko)");
            break;
        case NVOS_AGP_CONFIG_NVAGP:
            sprintf(agp_driver, "nvidia");
            break;
    }

    return SYSCTL_OUT(req, agp_driver, strlen(agp_driver) + 1);
}

int nvidia_sysctl_agp_rate_status(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_rate[16];

    NvU32 tmp = nvidia_sys_agp_cmd(dev);

    if ((tmp & 0x100) && (tmp &= 0x07)) {
        sprintf(agp_rate, "%1dx",
            (nvidia_dev_agp_status(dev) & 0x08) ? (tmp << 2) : tmp);
    } else {
        sprintf(agp_rate, "n/a (disabled)");
    }

    return SYSCTL_OUT(req, agp_rate, strlen(agp_rate) + 1);
}

int nvidia_sysctl_agp_fw_status(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_fw[16];

    NvU32 tmp = nvidia_sys_agp_cmd(dev);

    if ((tmp & 0x100) == 0x100) {
        sprintf(agp_fw, "%s",
            ((tmp & 0x10) == 0x10) ? "enabled" : "disabled");
    } else {
        sprintf(agp_fw, "n/a (disabled)");
    }

    return SYSCTL_OUT(req, agp_fw, strlen(agp_fw) + 1);
}

int nvidia_sysctl_agp_sba_status(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_sba[16];

    NvU32 tmp = nvidia_sys_agp_cmd(dev);

    if ((tmp & 0x100) == 0x100) {
        sprintf(agp_sba, "%s",
            ((tmp & 0x200) == 0x200) ? "enabled" : "disabled");
    } else {
        sprintf(agp_sba, "n/a (disabled)");
    }

    return SYSCTL_OUT(req, agp_sba, strlen(agp_sba) + 1);
}

int nvidia_sysctl_agp_status(SYSCTL_HANDLER_ARGS)
{
    device_t dev = arg1;
    NvU8 agp_cmd[16];

    sprintf(agp_cmd, "%s",
        (nvidia_sys_agp_cmd(dev) & 0x100) ? "enabled" : "disabled");

    return SYSCTL_OUT(req, agp_cmd, strlen(agp_cmd) + 1);
}

void nv_sysctl_init(nv_state_t *nv)
{
    struct sysctl_oid *oid;
    struct nvidia_softc *sc = nv->os_state;

    char name[4];
    sprintf(name, "%d", device_get_unit(sc->dev));

    sysctl_ctx_init(&sc->sysctl_ctx);

    oid = SYSCTL_ADD_NODE(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid_nvidia),
            OID_AUTO,
            "gpus",
            CTLFLAG_RD | CTLFLAG_DYN,
            0,
            "NVIDIA SYSCTL GPUs Node");

    oid = SYSCTL_ADD_NODE(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid),
            OID_AUTO,
            name,
            CTLFLAG_RD | CTLFLAG_DYN,
            0,
            "NVIDIA SYSCTL GPU Node");

    SYSCTL_ADD_PROC(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid),
            OID_AUTO,
            "model",
            CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
            (void *) nv, 0,
            nvidia_sysctl_gpu_model,
            "A",
            "NVIDIA GPU Model Name");

    SYSCTL_ADD_UINT(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid),
            OID_AUTO,
            "irq",
            CTLTYPE_UINT | CTLFLAG_RD | CTLFLAG_DYN,
            &nv->interrupt_line,
            0,
            "NVIDIA GPU IRQ Number");

    SYSCTL_ADD_PROC(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid),
            OID_AUTO,
            "vbios",
            CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
            (void *) nv, 0,
            nvidia_sysctl_gpu_vbios,
            "A",
            "NVIDIA GPU VBIOS Version");

    SYSCTL_ADD_PROC(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid),
            OID_AUTO,
            "uuid",
            CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
            (void *) nv, 0,
            nvidia_sysctl_gpu_uuid,
            "A",
            "NVIDIA GPU UUID");

    SYSCTL_ADD_PROC(&sc->sysctl_ctx,
            SYSCTL_CHILDREN(oid),
            OID_AUTO,
            "type",
            CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
            (void *) sc, 0,
            nvidia_sysctl_bus_type,
            "A",
            "NVIDIA GPU Bus Type");


    if (nvidia_pci_find_capability(sc->dev, PCIR_CAP_ID_AGP) != 0) {
        /*
         * Assume this is the only AGP VGA device in the system
         * and report its capabilities (hw.nvidia.agp.card).
         */
        oid = SYSCTL_ADD_NODE(&sysctl_ctx,
                SYSCTL_CHILDREN(oid_agp),
                OID_AUTO,
                "card",
                CTLFLAG_RD | CTLFLAG_DYN,
                0,
                "NVIDIA AGP Device Node");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "rates",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) sc->dev, 0,
                nvidia_sysctl_agp_rates,
                "A",
                "NVIDIA AGP Device Rates Info");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "fw",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) sc->dev, 0,
                nvidia_sysctl_agp_fw,
                "A",
                "NVIDIA AGP Device FW Info");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "sba",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) sc->dev, 0,
                nvidia_sysctl_agp_sba,
                "A",
                "NVIDIA AGP Device SBA Info");

        SYSCTL_ADD_PROC(&sysctl_ctx,
                SYSCTL_CHILDREN(oid),
                OID_AUTO,
                "registers",
                CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                (void *) sc->dev, 0,
                nvidia_sysctl_agp_registers,
                "A",
                "NVIDIA AGP Device Registers");


        if (nvidia_find_bridge() != NULL) {
            /*
             * If we can find a bridge and what assume to be the
             * only AGP VGA device, report the AGP status.
             */
            oid = SYSCTL_ADD_NODE(&sysctl_ctx,
                    SYSCTL_CHILDREN(oid_agp),
                    OID_AUTO,
                    "status",
                    CTLFLAG_RD | CTLFLAG_DYN,
                    0,
                    "NVIDIA AGP Status Node");

            SYSCTL_ADD_PROC(&sysctl_ctx,
                    SYSCTL_CHILDREN(oid),
                    OID_AUTO,
                    "status",
                    CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                    (void *) sc->dev, 0,
                    nvidia_sysctl_agp_status,
                    "A",
                    "NVIDIA AGP Status Information");

            SYSCTL_ADD_PROC(&sysctl_ctx,
                    SYSCTL_CHILDREN(oid),
                    OID_AUTO,
                    "driver",
                    CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                    (void *) nv, 0,
                    nvidia_sysctl_agp_driver,
                    "A",
                    "NVIDIA AGP Driver Information");

            SYSCTL_ADD_PROC(&sysctl_ctx,
                    SYSCTL_CHILDREN(oid),
                    OID_AUTO,
                    "rate",
                    CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                    (void *) sc->dev, 0,
                    nvidia_sysctl_agp_rate_status,
                    "A",
                    "NVIDIA AGP Rate Status Information");

            SYSCTL_ADD_PROC(&sysctl_ctx,
                    SYSCTL_CHILDREN(oid),
                    OID_AUTO,
                    "fw",
                    CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                    (void *) sc->dev, 0,
                    nvidia_sysctl_agp_fw_status,
                    "A",
                    "NVIDIA AGP FW Status Information");

            SYSCTL_ADD_PROC(&sysctl_ctx,
                    SYSCTL_CHILDREN(oid),
                    OID_AUTO,
                    "sba",
                    CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_DYN,
                    (void *) sc->dev, 0,
                    nvidia_sysctl_agp_sba_status,
                    "A",
                    "NVIDIA AGP SBA Status Information");
        }
    }
}

void nv_sysctl_exit(nv_state_t *nv)
{
    struct nvidia_softc *sc = nv->os_state;
    sysctl_ctx_free(&sc->sysctl_ctx);
}
