/*
* 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 = kcalloc(leaves, sizeof(*intr->stat), GFP_KERNEL);
intr->mask = kcalloc(leaves, sizeof(*intr->mask), GFP_KERNEL);
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;
}