root/drivers/gpu/drm/nouveau/nvkm/core/intr.c
/*
 * Copyright 2021 Red Hat Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
#include <core/intr.h>
#include <core/device.h>
#include <core/subdev.h>
#include <subdev/pci.h>
#include <subdev/top.h>

static int
nvkm_intr_xlat(struct nvkm_subdev *subdev, struct nvkm_intr *intr,
               enum nvkm_intr_type type, int *leaf, u32 *mask)
{
        struct nvkm_device *device = subdev->device;

        if (type < NVKM_INTR_VECTOR_0) {
                if (type == NVKM_INTR_SUBDEV) {
                        const struct nvkm_intr_data *data = intr->data;
                        struct nvkm_top_device *tdev;

                        while (data && data->mask) {
                                if (data->type == NVKM_SUBDEV_TOP) {
                                        list_for_each_entry(tdev, &device->top->device, head) {
                                                if (tdev->intr >= 0 &&
                                                    tdev->type == subdev->type &&
                                                    tdev->inst == subdev->inst) {
                                                        if (data->mask & BIT(tdev->intr)) {
                                                                *leaf = data->leaf;
                                                                *mask = BIT(tdev->intr);
                                                                return 0;
                                                        }
                                                }
                                        }
                                } else
                                if (data->type == subdev->type && data->inst == subdev->inst) {
                                        *leaf = data->leaf;
                                        *mask = data->mask;
                                        return 0;
                                }

                                data++;
                        }
                } else {
                        return -ENOSYS;
                }
        } else {
                if (type < intr->leaves * sizeof(*intr->stat) * 8) {
                        *leaf = type / 32;
                        *mask = BIT(type % 32);
                        return 0;
                }
        }

        return -EINVAL;
}

static struct nvkm_intr *
nvkm_intr_find(struct nvkm_subdev *subdev, enum nvkm_intr_type type, int *leaf, u32 *mask)
{
        struct nvkm_intr *intr;
        int ret;

        list_for_each_entry(intr, &subdev->device->intr.intr, head) {
                ret = nvkm_intr_xlat(subdev, intr, type, leaf, mask);
                if (ret == 0)
                        return intr;
        }

        return NULL;
}

static void
nvkm_intr_allow_locked(struct nvkm_intr *intr, int leaf, u32 mask)
{
        intr->mask[leaf] |= mask;
        if (intr->func->allow) {
                if (intr->func->reset)
                        intr->func->reset(intr, leaf, mask);
                intr->func->allow(intr, leaf, mask);
        }
}

void
nvkm_intr_allow(struct nvkm_subdev *subdev, enum nvkm_intr_type type)
{
        struct nvkm_device *device = subdev->device;
        struct nvkm_intr *intr;
        unsigned long flags;
        int leaf;
        u32 mask;

        intr = nvkm_intr_find(subdev, type, &leaf, &mask);
        if (intr) {
                nvkm_debug(intr->subdev, "intr %d/%08x allowed by %s\n", leaf, mask, subdev->name);
                spin_lock_irqsave(&device->intr.lock, flags);
                nvkm_intr_allow_locked(intr, leaf, mask);
                spin_unlock_irqrestore(&device->intr.lock, flags);
        }
}

static void
nvkm_intr_block_locked(struct nvkm_intr *intr, int leaf, u32 mask)
{
        intr->mask[leaf] &= ~mask;
        if (intr->func->block)
                intr->func->block(intr, leaf, mask);
}

void
nvkm_intr_block(struct nvkm_subdev *subdev, enum nvkm_intr_type type)
{
        struct nvkm_device *device = subdev->device;
        struct nvkm_intr *intr;
        unsigned long flags;
        int leaf;
        u32 mask;

        intr = nvkm_intr_find(subdev, type, &leaf, &mask);
        if (intr) {
                nvkm_debug(intr->subdev, "intr %d/%08x blocked by %s\n", leaf, mask, subdev->name);
                spin_lock_irqsave(&device->intr.lock, flags);
                nvkm_intr_block_locked(intr, leaf, mask);
                spin_unlock_irqrestore(&device->intr.lock, flags);
        }
}

static void
nvkm_intr_rearm_locked(struct nvkm_device *device)
{
        struct nvkm_intr *intr;

        list_for_each_entry(intr, &device->intr.intr, head)
                intr->func->rearm(intr);
}

static void
nvkm_intr_unarm_locked(struct nvkm_device *device)
{
        struct nvkm_intr *intr;

        list_for_each_entry(intr, &device->intr.intr, head)
                intr->func->unarm(intr);
}

static irqreturn_t
nvkm_intr(int irq, void *arg)
{
        struct nvkm_device *device = arg;
        struct nvkm_intr *intr;
        struct nvkm_inth *inth;
        irqreturn_t ret = IRQ_NONE;
        bool pending = false;
        int prio, leaf;

        /* Disable all top-level interrupt sources, and re-arm MSI interrupts. */
        spin_lock(&device->intr.lock);
        if (!device->intr.armed)
                goto done_unlock;

        nvkm_intr_unarm_locked(device);
        nvkm_pci_msi_rearm(device);

        /* Fetch pending interrupt masks. */
        list_for_each_entry(intr, &device->intr.intr, head) {
                if (intr->func->pending(intr))
                        pending = true;
        }

        if (!pending)
                goto done;

        /* Check that GPU is still on the bus by reading NV_PMC_BOOT_0. */
        if (WARN_ON(nvkm_rd32(device, 0x000000) == 0xffffffff))
                goto done;

        /* Execute handlers. */
        for (prio = 0; prio < ARRAY_SIZE(device->intr.prio); prio++) {
                list_for_each_entry(inth, &device->intr.prio[prio], head) {
                        struct nvkm_intr *intr = inth->intr;

                        if (intr->stat[inth->leaf] & inth->mask) {
                                if (atomic_read(&inth->allowed)) {
                                        if (intr->func->reset)
                                                intr->func->reset(intr, inth->leaf, inth->mask);
                                        if (inth->func(inth) == IRQ_HANDLED)
                                                ret = IRQ_HANDLED;
                                }
                        }
                }
        }

        /* Nothing handled?  Some debugging/protection from IRQ storms is in order... */
        if (ret == IRQ_NONE) {
                list_for_each_entry(intr, &device->intr.intr, head) {
                        for (leaf = 0; leaf < intr->leaves; leaf++) {
                                if (intr->stat[leaf]) {
                                        nvkm_debug(intr->subdev, "intr%d: %08x\n",
                                                   leaf, intr->stat[leaf]);
                                        nvkm_intr_block_locked(intr, leaf, intr->stat[leaf]);
                                }
                        }
                }
        }

done:
        /* Re-enable all top-level interrupt sources. */
        nvkm_intr_rearm_locked(device);
done_unlock:
        spin_unlock(&device->intr.lock);
        return ret;
}

int
nvkm_intr_add(const struct nvkm_intr_func *func, const struct nvkm_intr_data *data,
              struct nvkm_subdev *subdev, int leaves, struct nvkm_intr *intr)
{
        struct nvkm_device *device = subdev->device;
        int i;

        intr->func = func;
        intr->data = data;
        intr->subdev = subdev;
        intr->leaves = leaves;
        intr->stat = kzalloc_objs(*intr->stat, leaves);
        intr->mask = kzalloc_objs(*intr->mask, leaves);
        if (!intr->stat || !intr->mask) {
                kfree(intr->stat);
                return -ENOMEM;
        }

        if (intr->subdev->debug >= NV_DBG_DEBUG) {
                for (i = 0; i < intr->leaves; i++)
                        intr->mask[i] = ~0;
        }

        spin_lock_irq(&device->intr.lock);
        list_add_tail(&intr->head, &device->intr.intr);
        spin_unlock_irq(&device->intr.lock);
        return 0;
}

static irqreturn_t
nvkm_intr_subdev(struct nvkm_inth *inth)
{
        struct nvkm_subdev *subdev = container_of(inth, typeof(*subdev), inth);

        nvkm_subdev_intr(subdev);
        return IRQ_HANDLED;
}

static void
nvkm_intr_subdev_add_dev(struct nvkm_intr *intr, enum nvkm_subdev_type type, int inst)
{
        struct nvkm_subdev *subdev;
        enum nvkm_intr_prio prio;
        int ret;

        subdev = nvkm_device_subdev(intr->subdev->device, type, inst);
        if (!subdev || !subdev->func->intr)
                return;

        if (type == NVKM_ENGINE_DISP)
                prio = NVKM_INTR_PRIO_VBLANK;
        else
                prio = NVKM_INTR_PRIO_NORMAL;

        ret = nvkm_inth_add(intr, NVKM_INTR_SUBDEV, prio, subdev, nvkm_intr_subdev, &subdev->inth);
        if (WARN_ON(ret))
                return;

        nvkm_inth_allow(&subdev->inth);
}

static void
nvkm_intr_subdev_add(struct nvkm_intr *intr)
{
        const struct nvkm_intr_data *data;
        struct nvkm_device *device = intr->subdev->device;
        struct nvkm_top_device *tdev;

        for (data = intr->data; data && data->mask; data++) {
                if (data->legacy) {
                        if (data->type == NVKM_SUBDEV_TOP) {
                                list_for_each_entry(tdev, &device->top->device, head) {
                                        if (tdev->intr < 0 || !(data->mask & BIT(tdev->intr)))
                                                continue;

                                        nvkm_intr_subdev_add_dev(intr, tdev->type, tdev->inst);
                                }
                        } else {
                                nvkm_intr_subdev_add_dev(intr, data->type, data->inst);
                        }
                }
        }
}

void
nvkm_intr_rearm(struct nvkm_device *device)
{
        struct nvkm_intr *intr;
        int i;

        if (unlikely(!device->intr.legacy_done)) {
                list_for_each_entry(intr, &device->intr.intr, head)
                        nvkm_intr_subdev_add(intr);
                device->intr.legacy_done = true;
        }

        spin_lock_irq(&device->intr.lock);
        list_for_each_entry(intr, &device->intr.intr, head) {
                for (i = 0; intr->func->block && i < intr->leaves; i++) {
                        intr->func->block(intr, i, ~0);
                        intr->func->allow(intr, i, intr->mask[i]);
                }
        }

        nvkm_intr_rearm_locked(device);
        device->intr.armed = true;
        spin_unlock_irq(&device->intr.lock);
}

void
nvkm_intr_unarm(struct nvkm_device *device)
{
        spin_lock_irq(&device->intr.lock);
        nvkm_intr_unarm_locked(device);
        device->intr.armed = false;
        spin_unlock_irq(&device->intr.lock);
}

int
nvkm_intr_install(struct nvkm_device *device)
{
        int ret;

        device->intr.irq = device->func->irq(device);
        if (device->intr.irq < 0)
                return device->intr.irq;

        ret = request_irq(device->intr.irq, nvkm_intr, IRQF_SHARED, "nvkm", device);
        if (ret)
                return ret;

        device->intr.alloc = true;
        return 0;
}

void
nvkm_intr_dtor(struct nvkm_device *device)
{
        struct nvkm_intr *intr, *intt;

        list_for_each_entry_safe(intr, intt, &device->intr.intr, head) {
                list_del(&intr->head);
                kfree(intr->mask);
                kfree(intr->stat);
        }

        if (device->intr.alloc)
                free_irq(device->intr.irq, device);
}

void
nvkm_intr_ctor(struct nvkm_device *device)
{
        int i;

        INIT_LIST_HEAD(&device->intr.intr);
        for (i = 0; i < ARRAY_SIZE(device->intr.prio); i++)
                INIT_LIST_HEAD(&device->intr.prio[i]);

        spin_lock_init(&device->intr.lock);
        device->intr.armed = false;
}

void
nvkm_inth_block(struct nvkm_inth *inth)
{
        if (unlikely(!inth->intr))
                return;

        atomic_set(&inth->allowed, 0);
}

void
nvkm_inth_allow(struct nvkm_inth *inth)
{
        struct nvkm_intr *intr = inth->intr;
        unsigned long flags;

        if (unlikely(!inth->intr))
                return;

        spin_lock_irqsave(&intr->subdev->device->intr.lock, flags);
        if (!atomic_xchg(&inth->allowed, 1)) {
                if ((intr->mask[inth->leaf] & inth->mask) != inth->mask)
                        nvkm_intr_allow_locked(intr, inth->leaf, inth->mask);
        }
        spin_unlock_irqrestore(&intr->subdev->device->intr.lock, flags);
}

int
nvkm_inth_add(struct nvkm_intr *intr, enum nvkm_intr_type type, enum nvkm_intr_prio prio,
              struct nvkm_subdev *subdev, nvkm_inth_func func, struct nvkm_inth *inth)
{
        struct nvkm_device *device = subdev->device;
        int ret;

        if (WARN_ON(inth->mask))
                return -EBUSY;

        ret = nvkm_intr_xlat(subdev, intr, type, &inth->leaf, &inth->mask);
        if (ret)
                return ret;

        nvkm_debug(intr->subdev, "intr %d/%08x requested by %s\n",
                   inth->leaf, inth->mask, subdev->name);

        inth->intr = intr;
        inth->func = func;
        atomic_set(&inth->allowed, 0);
        list_add_tail(&inth->head, &device->intr.prio[prio]);
        return 0;
}