mirror of
https://github.com/torvalds/linux.git
synced 2024-11-22 12:11:40 +00:00
drm/panthor: Add the device logical block
The panthor driver is designed in a modular way, where each logical block is dealing with a specific HW-block or software feature. In order for those blocks to communicate with each other, we need a central panthor_device collecting all the blocks, and exposing some common features, like interrupt handling, power management, reset, ... This what this panthor_device logical block is about. v6: - Add Maxime's and Heiko's acks - Keep header inclusion alphabetically ordered v5: - Suspend the MMU/GPU blocks if panthor_fw_resume() fails in panthor_device_resume() - Move the pm_runtime_use_autosuspend() call before drm_dev_register() - Add Liviu's R-b v4: - Check drmm_mutex_init() return code - Fix panthor_device_reset_work() out path - Fix the race in the unplug logic - Fix typos - Unplug blocks when something fails in panthor_device_init() - Add Steve's R-b v3: - Add acks for the MIT+GPL2 relicensing - Fix 32-bit support - Shorten the sections protected by panthor_device::pm::mmio_lock to fix lock ordering issues. - Rename panthor_device::pm::lock into panthor_device::pm::mmio_lock to better reflect what this lock is protecting - Use dev_err_probe() - Make sure we call drm_dev_exit() when something fails half-way in panthor_device_reset_work() - Replace CSF_GPU_LATEST_FLUSH_ID_DEFAULT with a constant '1' and a comment to explain. Also remove setting the dummy flush ID on suspend. - Remove drm_WARN_ON() in panthor_exception_name() - Check pirq->suspended in panthor_xxx_irq_raw_handler() Co-developed-by: Steven Price <steven.price@arm.com> Signed-off-by: Steven Price <steven.price@arm.com> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com> Acked-by: Steven Price <steven.price@arm.com> # MIT+GPL2 relicensing,Arm Acked-by: Grant Likely <grant.likely@linaro.org> # MIT+GPL2 relicensing,Linaro Acked-by: Boris Brezillon <boris.brezillon@collabora.com> # MIT+GPL2 relicensing,Collabora Reviewed-by: Steven Price <steven.price@arm.com> Reviewed-by: Liviu Dudau <liviu.dudau@arm.com> Acked-by: Maxime Ripard <mripard@kernel.org> Acked-by: Heiko Stuebner <heiko@sntech.de> Link: https://patchwork.freedesktop.org/patch/msgid/20240229162230.2634044-4-boris.brezillon@collabora.com
This commit is contained in:
parent
546b366600
commit
5fe909cae1
549
drivers/gpu/drm/panthor/panthor_device.c
Normal file
549
drivers/gpu/drm/panthor/panthor_device.c
Normal file
@ -0,0 +1,549 @@
|
||||
// SPDX-License-Identifier: GPL-2.0 or MIT
|
||||
/* Copyright 2018 Marty E. Plummer <hanetzer@startmail.com> */
|
||||
/* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
|
||||
/* Copyright 2023 Collabora ltd. */
|
||||
|
||||
#include <linux/clk.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/pm_domain.h>
|
||||
#include <linux/pm_runtime.h>
|
||||
#include <linux/regulator/consumer.h>
|
||||
#include <linux/reset.h>
|
||||
|
||||
#include <drm/drm_drv.h>
|
||||
#include <drm/drm_managed.h>
|
||||
|
||||
#include "panthor_devfreq.h"
|
||||
#include "panthor_device.h"
|
||||
#include "panthor_fw.h"
|
||||
#include "panthor_gpu.h"
|
||||
#include "panthor_mmu.h"
|
||||
#include "panthor_regs.h"
|
||||
#include "panthor_sched.h"
|
||||
|
||||
static int panthor_clk_init(struct panthor_device *ptdev)
|
||||
{
|
||||
ptdev->clks.core = devm_clk_get(ptdev->base.dev, NULL);
|
||||
if (IS_ERR(ptdev->clks.core))
|
||||
return dev_err_probe(ptdev->base.dev,
|
||||
PTR_ERR(ptdev->clks.core),
|
||||
"get 'core' clock failed");
|
||||
|
||||
ptdev->clks.stacks = devm_clk_get_optional(ptdev->base.dev, "stacks");
|
||||
if (IS_ERR(ptdev->clks.stacks))
|
||||
return dev_err_probe(ptdev->base.dev,
|
||||
PTR_ERR(ptdev->clks.stacks),
|
||||
"get 'stacks' clock failed");
|
||||
|
||||
ptdev->clks.coregroup = devm_clk_get_optional(ptdev->base.dev, "coregroup");
|
||||
if (IS_ERR(ptdev->clks.coregroup))
|
||||
return dev_err_probe(ptdev->base.dev,
|
||||
PTR_ERR(ptdev->clks.coregroup),
|
||||
"get 'coregroup' clock failed");
|
||||
|
||||
drm_info(&ptdev->base, "clock rate = %lu\n", clk_get_rate(ptdev->clks.core));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void panthor_device_unplug(struct panthor_device *ptdev)
|
||||
{
|
||||
/* This function can be called from two different path: the reset work
|
||||
* and the platform device remove callback. drm_dev_unplug() doesn't
|
||||
* deal with concurrent callers, so we have to protect drm_dev_unplug()
|
||||
* calls with our own lock, and bail out if the device is already
|
||||
* unplugged.
|
||||
*/
|
||||
mutex_lock(&ptdev->unplug.lock);
|
||||
if (drm_dev_is_unplugged(&ptdev->base)) {
|
||||
/* Someone beat us, release the lock and wait for the unplug
|
||||
* operation to be reported as done.
|
||||
**/
|
||||
mutex_unlock(&ptdev->unplug.lock);
|
||||
wait_for_completion(&ptdev->unplug.done);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Call drm_dev_unplug() so any access to HW blocks happening after
|
||||
* that point get rejected.
|
||||
*/
|
||||
drm_dev_unplug(&ptdev->base);
|
||||
|
||||
/* We do the rest of the unplug with the unplug lock released,
|
||||
* future callers will wait on ptdev->unplug.done anyway.
|
||||
*/
|
||||
mutex_unlock(&ptdev->unplug.lock);
|
||||
|
||||
drm_WARN_ON(&ptdev->base, pm_runtime_get_sync(ptdev->base.dev) < 0);
|
||||
|
||||
/* Now, try to cleanly shutdown the GPU before the device resources
|
||||
* get reclaimed.
|
||||
*/
|
||||
panthor_sched_unplug(ptdev);
|
||||
panthor_fw_unplug(ptdev);
|
||||
panthor_mmu_unplug(ptdev);
|
||||
panthor_gpu_unplug(ptdev);
|
||||
|
||||
pm_runtime_dont_use_autosuspend(ptdev->base.dev);
|
||||
pm_runtime_put_sync_suspend(ptdev->base.dev);
|
||||
|
||||
/* Report the unplug operation as done to unblock concurrent
|
||||
* panthor_device_unplug() callers.
|
||||
*/
|
||||
complete_all(&ptdev->unplug.done);
|
||||
}
|
||||
|
||||
static void panthor_device_reset_cleanup(struct drm_device *ddev, void *data)
|
||||
{
|
||||
struct panthor_device *ptdev = container_of(ddev, struct panthor_device, base);
|
||||
|
||||
cancel_work_sync(&ptdev->reset.work);
|
||||
destroy_workqueue(ptdev->reset.wq);
|
||||
}
|
||||
|
||||
static void panthor_device_reset_work(struct work_struct *work)
|
||||
{
|
||||
struct panthor_device *ptdev = container_of(work, struct panthor_device, reset.work);
|
||||
int ret = 0, cookie;
|
||||
|
||||
if (atomic_read(&ptdev->pm.state) != PANTHOR_DEVICE_PM_STATE_ACTIVE) {
|
||||
/*
|
||||
* No need for a reset as the device has been (or will be)
|
||||
* powered down
|
||||
*/
|
||||
atomic_set(&ptdev->reset.pending, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!drm_dev_enter(&ptdev->base, &cookie))
|
||||
return;
|
||||
|
||||
panthor_sched_pre_reset(ptdev);
|
||||
panthor_fw_pre_reset(ptdev, true);
|
||||
panthor_mmu_pre_reset(ptdev);
|
||||
panthor_gpu_soft_reset(ptdev);
|
||||
panthor_gpu_l2_power_on(ptdev);
|
||||
panthor_mmu_post_reset(ptdev);
|
||||
ret = panthor_fw_post_reset(ptdev);
|
||||
if (ret)
|
||||
goto out_dev_exit;
|
||||
|
||||
atomic_set(&ptdev->reset.pending, 0);
|
||||
panthor_sched_post_reset(ptdev);
|
||||
|
||||
out_dev_exit:
|
||||
drm_dev_exit(cookie);
|
||||
|
||||
if (ret) {
|
||||
panthor_device_unplug(ptdev);
|
||||
drm_err(&ptdev->base, "Failed to boot MCU after reset, making device unusable.");
|
||||
}
|
||||
}
|
||||
|
||||
static bool panthor_device_is_initialized(struct panthor_device *ptdev)
|
||||
{
|
||||
return !!ptdev->scheduler;
|
||||
}
|
||||
|
||||
static void panthor_device_free_page(struct drm_device *ddev, void *data)
|
||||
{
|
||||
free_page((unsigned long)data);
|
||||
}
|
||||
|
||||
int panthor_device_init(struct panthor_device *ptdev)
|
||||
{
|
||||
struct resource *res;
|
||||
struct page *p;
|
||||
int ret;
|
||||
|
||||
ptdev->coherent = device_get_dma_attr(ptdev->base.dev) == DEV_DMA_COHERENT;
|
||||
|
||||
init_completion(&ptdev->unplug.done);
|
||||
ret = drmm_mutex_init(&ptdev->base, &ptdev->unplug.lock);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ret = drmm_mutex_init(&ptdev->base, &ptdev->pm.mmio_lock);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_SUSPENDED);
|
||||
p = alloc_page(GFP_KERNEL | __GFP_ZERO);
|
||||
if (!p)
|
||||
return -ENOMEM;
|
||||
|
||||
ptdev->pm.dummy_latest_flush = page_address(p);
|
||||
ret = drmm_add_action_or_reset(&ptdev->base, panthor_device_free_page,
|
||||
ptdev->pm.dummy_latest_flush);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
/*
|
||||
* Set the dummy page holding the latest flush to 1. This will cause the
|
||||
* flush to avoided as we know it isn't necessary if the submission
|
||||
* happens while the dummy page is mapped. Zero cannot be used because
|
||||
* that means 'always flush'.
|
||||
*/
|
||||
*ptdev->pm.dummy_latest_flush = 1;
|
||||
|
||||
INIT_WORK(&ptdev->reset.work, panthor_device_reset_work);
|
||||
ptdev->reset.wq = alloc_ordered_workqueue("panthor-reset-wq", 0);
|
||||
if (!ptdev->reset.wq)
|
||||
return -ENOMEM;
|
||||
|
||||
ret = drmm_add_action_or_reset(&ptdev->base, panthor_device_reset_cleanup, NULL);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ret = panthor_clk_init(ptdev);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ret = panthor_devfreq_init(ptdev);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ptdev->iomem = devm_platform_get_and_ioremap_resource(to_platform_device(ptdev->base.dev),
|
||||
0, &res);
|
||||
if (IS_ERR(ptdev->iomem))
|
||||
return PTR_ERR(ptdev->iomem);
|
||||
|
||||
ptdev->phys_addr = res->start;
|
||||
|
||||
ret = devm_pm_runtime_enable(ptdev->base.dev);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ret = pm_runtime_resume_and_get(ptdev->base.dev);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ret = panthor_gpu_init(ptdev);
|
||||
if (ret)
|
||||
goto err_rpm_put;
|
||||
|
||||
ret = panthor_mmu_init(ptdev);
|
||||
if (ret)
|
||||
goto err_unplug_gpu;
|
||||
|
||||
ret = panthor_fw_init(ptdev);
|
||||
if (ret)
|
||||
goto err_unplug_mmu;
|
||||
|
||||
ret = panthor_sched_init(ptdev);
|
||||
if (ret)
|
||||
goto err_unplug_fw;
|
||||
|
||||
/* ~3 frames */
|
||||
pm_runtime_set_autosuspend_delay(ptdev->base.dev, 50);
|
||||
pm_runtime_use_autosuspend(ptdev->base.dev);
|
||||
|
||||
ret = drm_dev_register(&ptdev->base, 0);
|
||||
if (ret)
|
||||
goto err_disable_autosuspend;
|
||||
|
||||
pm_runtime_put_autosuspend(ptdev->base.dev);
|
||||
return 0;
|
||||
|
||||
err_disable_autosuspend:
|
||||
pm_runtime_dont_use_autosuspend(ptdev->base.dev);
|
||||
panthor_sched_unplug(ptdev);
|
||||
|
||||
err_unplug_fw:
|
||||
panthor_fw_unplug(ptdev);
|
||||
|
||||
err_unplug_mmu:
|
||||
panthor_mmu_unplug(ptdev);
|
||||
|
||||
err_unplug_gpu:
|
||||
panthor_gpu_unplug(ptdev);
|
||||
|
||||
err_rpm_put:
|
||||
pm_runtime_put_sync_suspend(ptdev->base.dev);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#define PANTHOR_EXCEPTION(id) \
|
||||
[DRM_PANTHOR_EXCEPTION_ ## id] = { \
|
||||
.name = #id, \
|
||||
}
|
||||
|
||||
struct panthor_exception_info {
|
||||
const char *name;
|
||||
};
|
||||
|
||||
static const struct panthor_exception_info panthor_exception_infos[] = {
|
||||
PANTHOR_EXCEPTION(OK),
|
||||
PANTHOR_EXCEPTION(TERMINATED),
|
||||
PANTHOR_EXCEPTION(KABOOM),
|
||||
PANTHOR_EXCEPTION(EUREKA),
|
||||
PANTHOR_EXCEPTION(ACTIVE),
|
||||
PANTHOR_EXCEPTION(CS_RES_TERM),
|
||||
PANTHOR_EXCEPTION(CS_CONFIG_FAULT),
|
||||
PANTHOR_EXCEPTION(CS_ENDPOINT_FAULT),
|
||||
PANTHOR_EXCEPTION(CS_BUS_FAULT),
|
||||
PANTHOR_EXCEPTION(CS_INSTR_INVALID),
|
||||
PANTHOR_EXCEPTION(CS_CALL_STACK_OVERFLOW),
|
||||
PANTHOR_EXCEPTION(CS_INHERIT_FAULT),
|
||||
PANTHOR_EXCEPTION(INSTR_INVALID_PC),
|
||||
PANTHOR_EXCEPTION(INSTR_INVALID_ENC),
|
||||
PANTHOR_EXCEPTION(INSTR_BARRIER_FAULT),
|
||||
PANTHOR_EXCEPTION(DATA_INVALID_FAULT),
|
||||
PANTHOR_EXCEPTION(TILE_RANGE_FAULT),
|
||||
PANTHOR_EXCEPTION(ADDR_RANGE_FAULT),
|
||||
PANTHOR_EXCEPTION(IMPRECISE_FAULT),
|
||||
PANTHOR_EXCEPTION(OOM),
|
||||
PANTHOR_EXCEPTION(CSF_FW_INTERNAL_ERROR),
|
||||
PANTHOR_EXCEPTION(CSF_RES_EVICTION_TIMEOUT),
|
||||
PANTHOR_EXCEPTION(GPU_BUS_FAULT),
|
||||
PANTHOR_EXCEPTION(GPU_SHAREABILITY_FAULT),
|
||||
PANTHOR_EXCEPTION(SYS_SHAREABILITY_FAULT),
|
||||
PANTHOR_EXCEPTION(GPU_CACHEABILITY_FAULT),
|
||||
PANTHOR_EXCEPTION(TRANSLATION_FAULT_0),
|
||||
PANTHOR_EXCEPTION(TRANSLATION_FAULT_1),
|
||||
PANTHOR_EXCEPTION(TRANSLATION_FAULT_2),
|
||||
PANTHOR_EXCEPTION(TRANSLATION_FAULT_3),
|
||||
PANTHOR_EXCEPTION(TRANSLATION_FAULT_4),
|
||||
PANTHOR_EXCEPTION(PERM_FAULT_0),
|
||||
PANTHOR_EXCEPTION(PERM_FAULT_1),
|
||||
PANTHOR_EXCEPTION(PERM_FAULT_2),
|
||||
PANTHOR_EXCEPTION(PERM_FAULT_3),
|
||||
PANTHOR_EXCEPTION(ACCESS_FLAG_1),
|
||||
PANTHOR_EXCEPTION(ACCESS_FLAG_2),
|
||||
PANTHOR_EXCEPTION(ACCESS_FLAG_3),
|
||||
PANTHOR_EXCEPTION(ADDR_SIZE_FAULT_IN),
|
||||
PANTHOR_EXCEPTION(ADDR_SIZE_FAULT_OUT0),
|
||||
PANTHOR_EXCEPTION(ADDR_SIZE_FAULT_OUT1),
|
||||
PANTHOR_EXCEPTION(ADDR_SIZE_FAULT_OUT2),
|
||||
PANTHOR_EXCEPTION(ADDR_SIZE_FAULT_OUT3),
|
||||
PANTHOR_EXCEPTION(MEM_ATTR_FAULT_0),
|
||||
PANTHOR_EXCEPTION(MEM_ATTR_FAULT_1),
|
||||
PANTHOR_EXCEPTION(MEM_ATTR_FAULT_2),
|
||||
PANTHOR_EXCEPTION(MEM_ATTR_FAULT_3),
|
||||
};
|
||||
|
||||
const char *panthor_exception_name(struct panthor_device *ptdev, u32 exception_code)
|
||||
{
|
||||
if (exception_code >= ARRAY_SIZE(panthor_exception_infos) ||
|
||||
!panthor_exception_infos[exception_code].name)
|
||||
return "Unknown exception type";
|
||||
|
||||
return panthor_exception_infos[exception_code].name;
|
||||
}
|
||||
|
||||
static vm_fault_t panthor_mmio_vm_fault(struct vm_fault *vmf)
|
||||
{
|
||||
struct vm_area_struct *vma = vmf->vma;
|
||||
struct panthor_device *ptdev = vma->vm_private_data;
|
||||
u64 id = (u64)vma->vm_pgoff << PAGE_SHIFT;
|
||||
unsigned long pfn;
|
||||
pgprot_t pgprot;
|
||||
vm_fault_t ret;
|
||||
bool active;
|
||||
int cookie;
|
||||
|
||||
if (!drm_dev_enter(&ptdev->base, &cookie))
|
||||
return VM_FAULT_SIGBUS;
|
||||
|
||||
mutex_lock(&ptdev->pm.mmio_lock);
|
||||
active = atomic_read(&ptdev->pm.state) == PANTHOR_DEVICE_PM_STATE_ACTIVE;
|
||||
|
||||
switch (panthor_device_mmio_offset(id)) {
|
||||
case DRM_PANTHOR_USER_FLUSH_ID_MMIO_OFFSET:
|
||||
if (active)
|
||||
pfn = __phys_to_pfn(ptdev->phys_addr + CSF_GPU_LATEST_FLUSH_ID);
|
||||
else
|
||||
pfn = virt_to_pfn(ptdev->pm.dummy_latest_flush);
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = VM_FAULT_SIGBUS;
|
||||
goto out_unlock;
|
||||
}
|
||||
|
||||
pgprot = vma->vm_page_prot;
|
||||
if (active)
|
||||
pgprot = pgprot_noncached(pgprot);
|
||||
|
||||
ret = vmf_insert_pfn_prot(vma, vmf->address, pfn, pgprot);
|
||||
|
||||
out_unlock:
|
||||
mutex_unlock(&ptdev->pm.mmio_lock);
|
||||
drm_dev_exit(cookie);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static const struct vm_operations_struct panthor_mmio_vm_ops = {
|
||||
.fault = panthor_mmio_vm_fault,
|
||||
};
|
||||
|
||||
int panthor_device_mmap_io(struct panthor_device *ptdev, struct vm_area_struct *vma)
|
||||
{
|
||||
u64 id = (u64)vma->vm_pgoff << PAGE_SHIFT;
|
||||
|
||||
switch (panthor_device_mmio_offset(id)) {
|
||||
case DRM_PANTHOR_USER_FLUSH_ID_MMIO_OFFSET:
|
||||
if (vma->vm_end - vma->vm_start != PAGE_SIZE ||
|
||||
(vma->vm_flags & (VM_WRITE | VM_EXEC)))
|
||||
return -EINVAL;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Defer actual mapping to the fault handler. */
|
||||
vma->vm_private_data = ptdev;
|
||||
vma->vm_ops = &panthor_mmio_vm_ops;
|
||||
vm_flags_set(vma,
|
||||
VM_IO | VM_DONTCOPY | VM_DONTEXPAND |
|
||||
VM_NORESERVE | VM_DONTDUMP | VM_PFNMAP);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_PM
|
||||
int panthor_device_resume(struct device *dev)
|
||||
{
|
||||
struct panthor_device *ptdev = dev_get_drvdata(dev);
|
||||
int ret, cookie;
|
||||
|
||||
if (atomic_read(&ptdev->pm.state) != PANTHOR_DEVICE_PM_STATE_SUSPENDED)
|
||||
return -EINVAL;
|
||||
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_RESUMING);
|
||||
|
||||
ret = clk_prepare_enable(ptdev->clks.core);
|
||||
if (ret)
|
||||
goto err_set_suspended;
|
||||
|
||||
ret = clk_prepare_enable(ptdev->clks.stacks);
|
||||
if (ret)
|
||||
goto err_disable_core_clk;
|
||||
|
||||
ret = clk_prepare_enable(ptdev->clks.coregroup);
|
||||
if (ret)
|
||||
goto err_disable_stacks_clk;
|
||||
|
||||
ret = panthor_devfreq_resume(ptdev);
|
||||
if (ret)
|
||||
goto err_disable_coregroup_clk;
|
||||
|
||||
if (panthor_device_is_initialized(ptdev) &&
|
||||
drm_dev_enter(&ptdev->base, &cookie)) {
|
||||
panthor_gpu_resume(ptdev);
|
||||
panthor_mmu_resume(ptdev);
|
||||
ret = drm_WARN_ON(&ptdev->base, panthor_fw_resume(ptdev));
|
||||
if (!ret) {
|
||||
panthor_sched_resume(ptdev);
|
||||
} else {
|
||||
panthor_mmu_suspend(ptdev);
|
||||
panthor_gpu_suspend(ptdev);
|
||||
}
|
||||
|
||||
drm_dev_exit(cookie);
|
||||
|
||||
if (ret)
|
||||
goto err_suspend_devfreq;
|
||||
}
|
||||
|
||||
if (atomic_read(&ptdev->reset.pending))
|
||||
queue_work(ptdev->reset.wq, &ptdev->reset.work);
|
||||
|
||||
/* Clear all IOMEM mappings pointing to this device after we've
|
||||
* resumed. This way the fake mappings pointing to the dummy pages
|
||||
* are removed and the real iomem mapping will be restored on next
|
||||
* access.
|
||||
*/
|
||||
mutex_lock(&ptdev->pm.mmio_lock);
|
||||
unmap_mapping_range(ptdev->base.anon_inode->i_mapping,
|
||||
DRM_PANTHOR_USER_MMIO_OFFSET, 0, 1);
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_ACTIVE);
|
||||
mutex_unlock(&ptdev->pm.mmio_lock);
|
||||
return 0;
|
||||
|
||||
err_suspend_devfreq:
|
||||
panthor_devfreq_suspend(ptdev);
|
||||
|
||||
err_disable_coregroup_clk:
|
||||
clk_disable_unprepare(ptdev->clks.coregroup);
|
||||
|
||||
err_disable_stacks_clk:
|
||||
clk_disable_unprepare(ptdev->clks.stacks);
|
||||
|
||||
err_disable_core_clk:
|
||||
clk_disable_unprepare(ptdev->clks.core);
|
||||
|
||||
err_set_suspended:
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_SUSPENDED);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int panthor_device_suspend(struct device *dev)
|
||||
{
|
||||
struct panthor_device *ptdev = dev_get_drvdata(dev);
|
||||
int ret, cookie;
|
||||
|
||||
if (atomic_read(&ptdev->pm.state) != PANTHOR_DEVICE_PM_STATE_ACTIVE)
|
||||
return -EINVAL;
|
||||
|
||||
/* Clear all IOMEM mappings pointing to this device before we
|
||||
* shutdown the power-domain and clocks. Failing to do that results
|
||||
* in external aborts when the process accesses the iomem region.
|
||||
* We change the state and call unmap_mapping_range() with the
|
||||
* mmio_lock held to make sure the vm_fault handler won't set up
|
||||
* invalid mappings.
|
||||
*/
|
||||
mutex_lock(&ptdev->pm.mmio_lock);
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_SUSPENDING);
|
||||
unmap_mapping_range(ptdev->base.anon_inode->i_mapping,
|
||||
DRM_PANTHOR_USER_MMIO_OFFSET, 0, 1);
|
||||
mutex_unlock(&ptdev->pm.mmio_lock);
|
||||
|
||||
if (panthor_device_is_initialized(ptdev) &&
|
||||
drm_dev_enter(&ptdev->base, &cookie)) {
|
||||
cancel_work_sync(&ptdev->reset.work);
|
||||
|
||||
/* We prepare everything as if we were resetting the GPU.
|
||||
* The end of the reset will happen in the resume path though.
|
||||
*/
|
||||
panthor_sched_suspend(ptdev);
|
||||
panthor_fw_suspend(ptdev);
|
||||
panthor_mmu_suspend(ptdev);
|
||||
panthor_gpu_suspend(ptdev);
|
||||
drm_dev_exit(cookie);
|
||||
}
|
||||
|
||||
ret = panthor_devfreq_suspend(ptdev);
|
||||
if (ret) {
|
||||
if (panthor_device_is_initialized(ptdev) &&
|
||||
drm_dev_enter(&ptdev->base, &cookie)) {
|
||||
panthor_gpu_resume(ptdev);
|
||||
panthor_mmu_resume(ptdev);
|
||||
drm_WARN_ON(&ptdev->base, panthor_fw_resume(ptdev));
|
||||
panthor_sched_resume(ptdev);
|
||||
drm_dev_exit(cookie);
|
||||
}
|
||||
|
||||
goto err_set_active;
|
||||
}
|
||||
|
||||
clk_disable_unprepare(ptdev->clks.coregroup);
|
||||
clk_disable_unprepare(ptdev->clks.stacks);
|
||||
clk_disable_unprepare(ptdev->clks.core);
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_SUSPENDED);
|
||||
return 0;
|
||||
|
||||
err_set_active:
|
||||
/* If something failed and we have to revert back to an
|
||||
* active state, we also need to clear the MMIO userspace
|
||||
* mappings, so any dumb pages that were mapped while we
|
||||
* were trying to suspend gets invalidated.
|
||||
*/
|
||||
mutex_lock(&ptdev->pm.mmio_lock);
|
||||
atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_ACTIVE);
|
||||
unmap_mapping_range(ptdev->base.anon_inode->i_mapping,
|
||||
DRM_PANTHOR_USER_MMIO_OFFSET, 0, 1);
|
||||
mutex_unlock(&ptdev->pm.mmio_lock);
|
||||
return ret;
|
||||
}
|
||||
#endif
|
394
drivers/gpu/drm/panthor/panthor_device.h
Normal file
394
drivers/gpu/drm/panthor/panthor_device.h
Normal file
@ -0,0 +1,394 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0 or MIT */
|
||||
/* Copyright 2018 Marty E. Plummer <hanetzer@startmail.com> */
|
||||
/* Copyright 2019 Linaro, Ltd, Rob Herring <robh@kernel.org> */
|
||||
/* Copyright 2023 Collabora ltd. */
|
||||
|
||||
#ifndef __PANTHOR_DEVICE_H__
|
||||
#define __PANTHOR_DEVICE_H__
|
||||
|
||||
#include <linux/atomic.h>
|
||||
#include <linux/io-pgtable.h>
|
||||
#include <linux/regulator/consumer.h>
|
||||
#include <linux/sched.h>
|
||||
#include <linux/spinlock.h>
|
||||
|
||||
#include <drm/drm_device.h>
|
||||
#include <drm/drm_mm.h>
|
||||
#include <drm/gpu_scheduler.h>
|
||||
#include <drm/panthor_drm.h>
|
||||
|
||||
struct panthor_csf;
|
||||
struct panthor_csf_ctx;
|
||||
struct panthor_device;
|
||||
struct panthor_gpu;
|
||||
struct panthor_group_pool;
|
||||
struct panthor_heap_pool;
|
||||
struct panthor_job;
|
||||
struct panthor_mmu;
|
||||
struct panthor_fw;
|
||||
struct panthor_perfcnt;
|
||||
struct panthor_vm;
|
||||
struct panthor_vm_pool;
|
||||
|
||||
/**
|
||||
* enum panthor_device_pm_state - PM state
|
||||
*/
|
||||
enum panthor_device_pm_state {
|
||||
/** @PANTHOR_DEVICE_PM_STATE_SUSPENDED: Device is suspended. */
|
||||
PANTHOR_DEVICE_PM_STATE_SUSPENDED = 0,
|
||||
|
||||
/** @PANTHOR_DEVICE_PM_STATE_RESUMING: Device is being resumed. */
|
||||
PANTHOR_DEVICE_PM_STATE_RESUMING,
|
||||
|
||||
/** @PANTHOR_DEVICE_PM_STATE_ACTIVE: Device is active. */
|
||||
PANTHOR_DEVICE_PM_STATE_ACTIVE,
|
||||
|
||||
/** @PANTHOR_DEVICE_PM_STATE_SUSPENDING: Device is being suspended. */
|
||||
PANTHOR_DEVICE_PM_STATE_SUSPENDING,
|
||||
};
|
||||
|
||||
/**
|
||||
* struct panthor_irq - IRQ data
|
||||
*
|
||||
* Used to automate IRQ handling for the 3 different IRQs we have in this driver.
|
||||
*/
|
||||
struct panthor_irq {
|
||||
/** @ptdev: Panthor device */
|
||||
struct panthor_device *ptdev;
|
||||
|
||||
/** @irq: IRQ number. */
|
||||
int irq;
|
||||
|
||||
/** @mask: Current mask being applied to xxx_INT_MASK. */
|
||||
u32 mask;
|
||||
|
||||
/** @suspended: Set to true when the IRQ is suspended. */
|
||||
atomic_t suspended;
|
||||
};
|
||||
|
||||
/**
|
||||
* struct panthor_device - Panthor device
|
||||
*/
|
||||
struct panthor_device {
|
||||
/** @base: Base drm_device. */
|
||||
struct drm_device base;
|
||||
|
||||
/** @phys_addr: Physical address of the iomem region. */
|
||||
phys_addr_t phys_addr;
|
||||
|
||||
/** @iomem: CPU mapping of the IOMEM region. */
|
||||
void __iomem *iomem;
|
||||
|
||||
/** @clks: GPU clocks. */
|
||||
struct {
|
||||
/** @core: Core clock. */
|
||||
struct clk *core;
|
||||
|
||||
/** @stacks: Stacks clock. This clock is optional. */
|
||||
struct clk *stacks;
|
||||
|
||||
/** @coregroup: Core group clock. This clock is optional. */
|
||||
struct clk *coregroup;
|
||||
} clks;
|
||||
|
||||
/** @coherent: True if the CPU/GPU are memory coherent. */
|
||||
bool coherent;
|
||||
|
||||
/** @gpu_info: GPU information. */
|
||||
struct drm_panthor_gpu_info gpu_info;
|
||||
|
||||
/** @csif_info: Command stream interface information. */
|
||||
struct drm_panthor_csif_info csif_info;
|
||||
|
||||
/** @gpu: GPU management data. */
|
||||
struct panthor_gpu *gpu;
|
||||
|
||||
/** @fw: FW management data. */
|
||||
struct panthor_fw *fw;
|
||||
|
||||
/** @mmu: MMU management data. */
|
||||
struct panthor_mmu *mmu;
|
||||
|
||||
/** @scheduler: Scheduler management data. */
|
||||
struct panthor_scheduler *scheduler;
|
||||
|
||||
/** @devfreq: Device frequency scaling management data. */
|
||||
struct panthor_devfreq *devfreq;
|
||||
|
||||
/** @unplug: Device unplug related fields. */
|
||||
struct {
|
||||
/** @lock: Lock used to serialize unplug operations. */
|
||||
struct mutex lock;
|
||||
|
||||
/**
|
||||
* @done: Completion object signaled when the unplug
|
||||
* operation is done.
|
||||
*/
|
||||
struct completion done;
|
||||
} unplug;
|
||||
|
||||
/** @reset: Reset related fields. */
|
||||
struct {
|
||||
/** @wq: Ordered worqueud used to schedule reset operations. */
|
||||
struct workqueue_struct *wq;
|
||||
|
||||
/** @work: Reset work. */
|
||||
struct work_struct work;
|
||||
|
||||
/** @pending: Set to true if a reset is pending. */
|
||||
atomic_t pending;
|
||||
} reset;
|
||||
|
||||
/** @pm: Power management related data. */
|
||||
struct {
|
||||
/** @state: Power state. */
|
||||
atomic_t state;
|
||||
|
||||
/**
|
||||
* @mmio_lock: Lock protecting MMIO userspace CPU mappings.
|
||||
*
|
||||
* This is needed to ensure we map the dummy IO pages when
|
||||
* the device is being suspended, and the real IO pages when
|
||||
* the device is being resumed. We can't just do with the
|
||||
* state atomicity to deal with this race.
|
||||
*/
|
||||
struct mutex mmio_lock;
|
||||
|
||||
/**
|
||||
* @dummy_latest_flush: Dummy LATEST_FLUSH page.
|
||||
*
|
||||
* Used to replace the real LATEST_FLUSH page when the GPU
|
||||
* is suspended.
|
||||
*/
|
||||
u32 *dummy_latest_flush;
|
||||
} pm;
|
||||
};
|
||||
|
||||
/**
|
||||
* struct panthor_file - Panthor file
|
||||
*/
|
||||
struct panthor_file {
|
||||
/** @ptdev: Device attached to this file. */
|
||||
struct panthor_device *ptdev;
|
||||
|
||||
/** @vms: VM pool attached to this file. */
|
||||
struct panthor_vm_pool *vms;
|
||||
|
||||
/** @groups: Scheduling group pool attached to this file. */
|
||||
struct panthor_group_pool *groups;
|
||||
};
|
||||
|
||||
int panthor_device_init(struct panthor_device *ptdev);
|
||||
void panthor_device_unplug(struct panthor_device *ptdev);
|
||||
|
||||
/**
|
||||
* panthor_device_schedule_reset() - Schedules a reset operation
|
||||
*/
|
||||
static inline void panthor_device_schedule_reset(struct panthor_device *ptdev)
|
||||
{
|
||||
if (!atomic_cmpxchg(&ptdev->reset.pending, 0, 1) &&
|
||||
atomic_read(&ptdev->pm.state) == PANTHOR_DEVICE_PM_STATE_ACTIVE)
|
||||
queue_work(ptdev->reset.wq, &ptdev->reset.work);
|
||||
}
|
||||
|
||||
/**
|
||||
* panthor_device_reset_is_pending() - Checks if a reset is pending.
|
||||
*
|
||||
* Return: true if a reset is pending, false otherwise.
|
||||
*/
|
||||
static inline bool panthor_device_reset_is_pending(struct panthor_device *ptdev)
|
||||
{
|
||||
return atomic_read(&ptdev->reset.pending) != 0;
|
||||
}
|
||||
|
||||
int panthor_device_mmap_io(struct panthor_device *ptdev,
|
||||
struct vm_area_struct *vma);
|
||||
|
||||
int panthor_device_resume(struct device *dev);
|
||||
int panthor_device_suspend(struct device *dev);
|
||||
|
||||
enum drm_panthor_exception_type {
|
||||
DRM_PANTHOR_EXCEPTION_OK = 0x00,
|
||||
DRM_PANTHOR_EXCEPTION_TERMINATED = 0x04,
|
||||
DRM_PANTHOR_EXCEPTION_KABOOM = 0x05,
|
||||
DRM_PANTHOR_EXCEPTION_EUREKA = 0x06,
|
||||
DRM_PANTHOR_EXCEPTION_ACTIVE = 0x08,
|
||||
DRM_PANTHOR_EXCEPTION_CS_RES_TERM = 0x0f,
|
||||
DRM_PANTHOR_EXCEPTION_MAX_NON_FAULT = 0x3f,
|
||||
DRM_PANTHOR_EXCEPTION_CS_CONFIG_FAULT = 0x40,
|
||||
DRM_PANTHOR_EXCEPTION_CS_ENDPOINT_FAULT = 0x44,
|
||||
DRM_PANTHOR_EXCEPTION_CS_BUS_FAULT = 0x48,
|
||||
DRM_PANTHOR_EXCEPTION_CS_INSTR_INVALID = 0x49,
|
||||
DRM_PANTHOR_EXCEPTION_CS_CALL_STACK_OVERFLOW = 0x4a,
|
||||
DRM_PANTHOR_EXCEPTION_CS_INHERIT_FAULT = 0x4b,
|
||||
DRM_PANTHOR_EXCEPTION_INSTR_INVALID_PC = 0x50,
|
||||
DRM_PANTHOR_EXCEPTION_INSTR_INVALID_ENC = 0x51,
|
||||
DRM_PANTHOR_EXCEPTION_INSTR_BARRIER_FAULT = 0x55,
|
||||
DRM_PANTHOR_EXCEPTION_DATA_INVALID_FAULT = 0x58,
|
||||
DRM_PANTHOR_EXCEPTION_TILE_RANGE_FAULT = 0x59,
|
||||
DRM_PANTHOR_EXCEPTION_ADDR_RANGE_FAULT = 0x5a,
|
||||
DRM_PANTHOR_EXCEPTION_IMPRECISE_FAULT = 0x5b,
|
||||
DRM_PANTHOR_EXCEPTION_OOM = 0x60,
|
||||
DRM_PANTHOR_EXCEPTION_CSF_FW_INTERNAL_ERROR = 0x68,
|
||||
DRM_PANTHOR_EXCEPTION_CSF_RES_EVICTION_TIMEOUT = 0x69,
|
||||
DRM_PANTHOR_EXCEPTION_GPU_BUS_FAULT = 0x80,
|
||||
DRM_PANTHOR_EXCEPTION_GPU_SHAREABILITY_FAULT = 0x88,
|
||||
DRM_PANTHOR_EXCEPTION_SYS_SHAREABILITY_FAULT = 0x89,
|
||||
DRM_PANTHOR_EXCEPTION_GPU_CACHEABILITY_FAULT = 0x8a,
|
||||
DRM_PANTHOR_EXCEPTION_TRANSLATION_FAULT_0 = 0xc0,
|
||||
DRM_PANTHOR_EXCEPTION_TRANSLATION_FAULT_1 = 0xc1,
|
||||
DRM_PANTHOR_EXCEPTION_TRANSLATION_FAULT_2 = 0xc2,
|
||||
DRM_PANTHOR_EXCEPTION_TRANSLATION_FAULT_3 = 0xc3,
|
||||
DRM_PANTHOR_EXCEPTION_TRANSLATION_FAULT_4 = 0xc4,
|
||||
DRM_PANTHOR_EXCEPTION_PERM_FAULT_0 = 0xc8,
|
||||
DRM_PANTHOR_EXCEPTION_PERM_FAULT_1 = 0xc9,
|
||||
DRM_PANTHOR_EXCEPTION_PERM_FAULT_2 = 0xca,
|
||||
DRM_PANTHOR_EXCEPTION_PERM_FAULT_3 = 0xcb,
|
||||
DRM_PANTHOR_EXCEPTION_ACCESS_FLAG_1 = 0xd9,
|
||||
DRM_PANTHOR_EXCEPTION_ACCESS_FLAG_2 = 0xda,
|
||||
DRM_PANTHOR_EXCEPTION_ACCESS_FLAG_3 = 0xdb,
|
||||
DRM_PANTHOR_EXCEPTION_ADDR_SIZE_FAULT_IN = 0xe0,
|
||||
DRM_PANTHOR_EXCEPTION_ADDR_SIZE_FAULT_OUT0 = 0xe4,
|
||||
DRM_PANTHOR_EXCEPTION_ADDR_SIZE_FAULT_OUT1 = 0xe5,
|
||||
DRM_PANTHOR_EXCEPTION_ADDR_SIZE_FAULT_OUT2 = 0xe6,
|
||||
DRM_PANTHOR_EXCEPTION_ADDR_SIZE_FAULT_OUT3 = 0xe7,
|
||||
DRM_PANTHOR_EXCEPTION_MEM_ATTR_FAULT_0 = 0xe8,
|
||||
DRM_PANTHOR_EXCEPTION_MEM_ATTR_FAULT_1 = 0xe9,
|
||||
DRM_PANTHOR_EXCEPTION_MEM_ATTR_FAULT_2 = 0xea,
|
||||
DRM_PANTHOR_EXCEPTION_MEM_ATTR_FAULT_3 = 0xeb,
|
||||
};
|
||||
|
||||
/**
|
||||
* panthor_exception_is_fault() - Checks if an exception is a fault.
|
||||
*
|
||||
* Return: true if the exception is a fault, false otherwise.
|
||||
*/
|
||||
static inline bool
|
||||
panthor_exception_is_fault(u32 exception_code)
|
||||
{
|
||||
return exception_code > DRM_PANTHOR_EXCEPTION_MAX_NON_FAULT;
|
||||
}
|
||||
|
||||
const char *panthor_exception_name(struct panthor_device *ptdev,
|
||||
u32 exception_code);
|
||||
|
||||
/**
|
||||
* PANTHOR_IRQ_HANDLER() - Define interrupt handlers and the interrupt
|
||||
* registration function.
|
||||
*
|
||||
* The boiler-plate to gracefully deal with shared interrupts is
|
||||
* auto-generated. All you have to do is call PANTHOR_IRQ_HANDLER()
|
||||
* just after the actual handler. The handler prototype is:
|
||||
*
|
||||
* void (*handler)(struct panthor_device *, u32 status);
|
||||
*/
|
||||
#define PANTHOR_IRQ_HANDLER(__name, __reg_prefix, __handler) \
|
||||
static irqreturn_t panthor_ ## __name ## _irq_raw_handler(int irq, void *data) \
|
||||
{ \
|
||||
struct panthor_irq *pirq = data; \
|
||||
struct panthor_device *ptdev = pirq->ptdev; \
|
||||
\
|
||||
if (atomic_read(&pirq->suspended)) \
|
||||
return IRQ_NONE; \
|
||||
if (!gpu_read(ptdev, __reg_prefix ## _INT_STAT)) \
|
||||
return IRQ_NONE; \
|
||||
\
|
||||
gpu_write(ptdev, __reg_prefix ## _INT_MASK, 0); \
|
||||
return IRQ_WAKE_THREAD; \
|
||||
} \
|
||||
\
|
||||
static irqreturn_t panthor_ ## __name ## _irq_threaded_handler(int irq, void *data) \
|
||||
{ \
|
||||
struct panthor_irq *pirq = data; \
|
||||
struct panthor_device *ptdev = pirq->ptdev; \
|
||||
irqreturn_t ret = IRQ_NONE; \
|
||||
\
|
||||
while (true) { \
|
||||
u32 status = gpu_read(ptdev, __reg_prefix ## _INT_RAWSTAT) & pirq->mask; \
|
||||
\
|
||||
if (!status) \
|
||||
break; \
|
||||
\
|
||||
gpu_write(ptdev, __reg_prefix ## _INT_CLEAR, status); \
|
||||
\
|
||||
__handler(ptdev, status); \
|
||||
ret = IRQ_HANDLED; \
|
||||
} \
|
||||
\
|
||||
if (!atomic_read(&pirq->suspended)) \
|
||||
gpu_write(ptdev, __reg_prefix ## _INT_MASK, pirq->mask); \
|
||||
\
|
||||
return ret; \
|
||||
} \
|
||||
\
|
||||
static inline void panthor_ ## __name ## _irq_suspend(struct panthor_irq *pirq) \
|
||||
{ \
|
||||
int cookie; \
|
||||
\
|
||||
atomic_set(&pirq->suspended, true); \
|
||||
\
|
||||
if (drm_dev_enter(&pirq->ptdev->base, &cookie)) { \
|
||||
gpu_write(pirq->ptdev, __reg_prefix ## _INT_MASK, 0); \
|
||||
synchronize_irq(pirq->irq); \
|
||||
drm_dev_exit(cookie); \
|
||||
} \
|
||||
\
|
||||
pirq->mask = 0; \
|
||||
} \
|
||||
\
|
||||
static inline void panthor_ ## __name ## _irq_resume(struct panthor_irq *pirq, u32 mask) \
|
||||
{ \
|
||||
int cookie; \
|
||||
\
|
||||
atomic_set(&pirq->suspended, false); \
|
||||
pirq->mask = mask; \
|
||||
\
|
||||
if (drm_dev_enter(&pirq->ptdev->base, &cookie)) { \
|
||||
gpu_write(pirq->ptdev, __reg_prefix ## _INT_CLEAR, mask); \
|
||||
gpu_write(pirq->ptdev, __reg_prefix ## _INT_MASK, mask); \
|
||||
drm_dev_exit(cookie); \
|
||||
} \
|
||||
} \
|
||||
\
|
||||
static int panthor_request_ ## __name ## _irq(struct panthor_device *ptdev, \
|
||||
struct panthor_irq *pirq, \
|
||||
int irq, u32 mask) \
|
||||
{ \
|
||||
pirq->ptdev = ptdev; \
|
||||
pirq->irq = irq; \
|
||||
panthor_ ## __name ## _irq_resume(pirq, mask); \
|
||||
\
|
||||
return devm_request_threaded_irq(ptdev->base.dev, irq, \
|
||||
panthor_ ## __name ## _irq_raw_handler, \
|
||||
panthor_ ## __name ## _irq_threaded_handler, \
|
||||
IRQF_SHARED, KBUILD_MODNAME "-" # __name, \
|
||||
pirq); \
|
||||
}
|
||||
|
||||
/**
|
||||
* panthor_device_mmio_offset() - Turn a user MMIO offset into a kernel one
|
||||
* @offset: Offset to convert.
|
||||
*
|
||||
* With 32-bit systems being limited by the 32-bit representation of mmap2's
|
||||
* pgoffset field, we need to make the MMIO offset arch specific. This function
|
||||
* converts a user MMIO offset into something the kernel driver understands.
|
||||
*
|
||||
* If the kernel and userspace architecture match, the offset is unchanged. If
|
||||
* the kernel is 64-bit and userspace is 32-bit, the offset is adjusted to match
|
||||
* 64-bit offsets. 32-bit kernel with 64-bit userspace is impossible.
|
||||
*
|
||||
* Return: Adjusted offset.
|
||||
*/
|
||||
static inline u64 panthor_device_mmio_offset(u64 offset)
|
||||
{
|
||||
#ifdef CONFIG_ARM64
|
||||
if (test_tsk_thread_flag(current, TIF_32BIT))
|
||||
offset += DRM_PANTHOR_USER_MMIO_OFFSET_64BIT - DRM_PANTHOR_USER_MMIO_OFFSET_32BIT;
|
||||
#endif
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
extern struct workqueue_struct *panthor_cleanup_wq;
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue
Block a user