// SPDX-License-Identifier: GPL-2.0-only OR MIT
/* Copyright 2026 Arm, Ltd. */
/* Based on v3d_perfmon.c, Copyright (C) 2021 Raspberry Pi */
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/pm_runtime.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <drm/drm_file.h>
#include <drm/drm_ioctl.h>
#include <uapi/drm/ethosu_accel.h>
#include "ethosu_drv.h"
#include "ethosu_device.h"
void ethosu_perfmon_get(struct ethosu_perfmon *perfmon)
{
if (perfmon)
refcount_inc(&perfmon->refcnt);
}
void ethosu_perfmon_put(struct ethosu_perfmon *perfmon)
{
if (perfmon && refcount_dec_and_test(&perfmon->refcnt))
kfree(perfmon);
}
void ethosu_perfmon_start(struct ethosu_device *ethosu, struct ethosu_perfmon *perfmon)
{
unsigned int i;
u8 ncounters;
u32 mask;
lockdep_assert_held(ðosu->perfmon_state.lock);
if (WARN_ON_ONCE(!perfmon || ethosu->perfmon_state.active))
return;
writel_relaxed(PMCR_CNT_EN, ethosu->pmu_regs + NPU_REG_PMCR);
writel_relaxed(PMU_EV_TYPE_CYCLES, ethosu->pmu_regs + NPU_REG_PMCCNTR_CFG);
mask = 0x80000000;
ncounters = perfmon->ncounters - 1;
if (ncounters)
mask |= GENMASK(ncounters - 1, 0);
for (i = 0; i < ncounters; i++)
writel_relaxed(perfmon->counters[i], ethosu->pmu_regs + NPU_REG_PMU_EVTYPER(i));
writel_relaxed(mask, ethosu->pmu_regs + NPU_REG_PMCNTENSET);
writel_relaxed(PMCR_CNT_EN | PMCR_EVENT_CNT_RST | PMCR_CYCLE_CNT_RST,
ethosu->pmu_regs + NPU_REG_PMCR);
ethosu->perfmon_state.active = perfmon;
}
void ethosu_perfmon_stop_locked(struct ethosu_device *ethosu, struct ethosu_perfmon *perfmon,
bool capture)
{
unsigned int i;
u8 ncounters;
u32 mask;
lockdep_assert_held(ðosu->perfmon_state.lock);
if (!perfmon || perfmon != ethosu->perfmon_state.active)
return;
ncounters = perfmon->ncounters - 1;
if (!pm_runtime_get_if_active(ethosu->base.dev)) {
ethosu->perfmon_state.active = NULL;
return;
}
if (capture) {
for (i = 0; i < ncounters; i++)
perfmon->values[i] += readl_relaxed(ethosu->pmu_regs + NPU_REG_PMU_EVCNTR(i));
perfmon->values[ncounters] +=
readl_relaxed(ethosu->pmu_regs + NPU_REG_PMCCNTR_LO) |
(u64)readl_relaxed(ethosu->pmu_regs + NPU_REG_PMCCNTR_HI) << 32;
}
mask = 0x80000000;
if (ncounters)
mask |= GENMASK(ncounters - 1, 0);
writel_relaxed(mask, ethosu->pmu_regs + NPU_REG_PMCNTENCLR);
writel_relaxed(0, ethosu->pmu_regs + NPU_REG_PMCR);
ethosu->perfmon_state.active = NULL;
pm_runtime_put(ethosu->base.dev);
}
void ethosu_perfmon_stop(struct ethosu_device *ethosu, struct ethosu_perfmon *perfmon,
bool capture)
{
if (!perfmon)
return;
guard(mutex)(ðosu->perfmon_state.lock);
ethosu_perfmon_stop_locked(ethosu, perfmon, capture);
}
struct ethosu_perfmon *ethosu_perfmon_find(struct ethosu_file_priv *ethosu_priv, int id)
{
struct ethosu_perfmon *perfmon;
xa_lock(ðosu_priv->perfmons);
perfmon = xa_load(ðosu_priv->perfmons, id);
ethosu_perfmon_get(perfmon);
xa_unlock(ðosu_priv->perfmons);
return perfmon;
}
void ethosu_perfmon_open_file(struct ethosu_file_priv *ethosu_priv)
{
xa_init_flags(ðosu_priv->perfmons, XA_FLAGS_ALLOC1);
}
static void ethosu_perfmon_delete(struct ethosu_file_priv *ethosu_priv,
struct ethosu_perfmon *perfmon)
{
struct ethosu_device *ethosu = ethosu_priv->edev;
/* If the active perfmon is being destroyed, stop it first */
scoped_guard(mutex, ðosu->perfmon_state.lock) {
/* If the global perfmon is being destroyed, set it to NULL */
if (ethosu->global_perfmon == perfmon) {
ethosu->global_perfmon = NULL;
ethosu_perfmon_put(perfmon);
}
ethosu_perfmon_stop_locked(ethosu, perfmon, false);
}
ethosu_perfmon_put(perfmon);
}
void ethosu_perfmon_close_file(struct ethosu_file_priv *ethosu_priv)
{
struct ethosu_perfmon *perfmon;
unsigned long id;
xa_for_each(ðosu_priv->perfmons, id, perfmon)
ethosu_perfmon_delete(ethosu_priv, perfmon);
xa_destroy(ðosu_priv->perfmons);
}
int ethosu_ioctl_perfmon_create(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_create *req = data;
struct ethosu_device *ethosu = to_ethosu_device(dev);
struct ethosu_perfmon *perfmon;
unsigned int i, event_max;
int ret;
u32 id;
/* Number of monitored counters cannot exceed HW limits. */
if (req->ncounters > ethosu->npu_info.pmu_counters)
return -EINVAL;
/* Make sure all counters are valid. */
event_max = ethosu_is_u65(ethosu) ? 433 : 671;
for (i = 0; i < req->ncounters; i++) {
if (req->counters[i] > event_max)
return -EINVAL;
}
/* Add 1 more counter for cycle counter */
req->ncounters++;
perfmon = kzalloc_flex(*perfmon, values, req->ncounters);
if (!perfmon)
return -ENOMEM;
for (i = 0; i < req->ncounters - 1; i++)
perfmon->counters[i] = req->counters[i];
perfmon->ncounters = req->ncounters;
refcount_set(&perfmon->refcnt, 1);
ret = xa_alloc(ðosu_priv->perfmons, &id, perfmon, xa_limit_32b,
GFP_KERNEL);
if (ret < 0) {
kfree(perfmon);
return ret;
}
req->id = id;
return 0;
}
int ethosu_ioctl_perfmon_destroy(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_destroy *req = data;
struct ethosu_perfmon *perfmon;
perfmon = xa_erase(ðosu_priv->perfmons, req->id);
if (!perfmon)
return -EINVAL;
ethosu_perfmon_delete(ethosu_priv, perfmon);
return 0;
}
int ethosu_ioctl_perfmon_get_values(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_device *ethosu = to_ethosu_device(dev);
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_get_values *req = data;
struct ethosu_perfmon *perfmon;
int ret = 0;
if (req->pad != 0)
return -EINVAL;
perfmon = ethosu_perfmon_find(ethosu_priv, req->id);
if (!perfmon)
return -EINVAL;
ret = pm_runtime_resume_and_get(dev->dev);
if (ret) {
ethosu_perfmon_put(perfmon);
return ret;
}
ethosu_perfmon_stop(ethosu, perfmon, true);
pm_runtime_put_autosuspend(dev->dev);
if (copy_to_user(u64_to_user_ptr(req->values_ptr), perfmon->values,
perfmon->ncounters * sizeof(u64)))
ret = -EFAULT;
ethosu_perfmon_put(perfmon);
return ret;
}
int ethosu_ioctl_perfmon_set_global(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_set_global *req = data;
struct ethosu_device *ethosu = to_ethosu_device(dev);
struct ethosu_perfmon *perfmon;
if (req->flags & ~DRM_ETHOSU_PERFMON_CLEAR_GLOBAL)
return -EINVAL;
perfmon = ethosu_perfmon_find(ethosu_priv, req->id);
if (!perfmon)
return -EINVAL;
/* If the request is to clear the global performance monitor */
if (req->flags & DRM_ETHOSU_PERFMON_CLEAR_GLOBAL) {
struct ethosu_perfmon *old;
scoped_guard(mutex, ðosu->perfmon_state.lock) {
old = ethosu->global_perfmon;
if (!old) {
ethosu_perfmon_put(perfmon);
return -EINVAL;
}
ethosu->global_perfmon = NULL;
ethosu_perfmon_stop_locked(ethosu, old, true);
}
ethosu_perfmon_put(old);
ethosu_perfmon_put(perfmon);
return 0;
}
scoped_guard(mutex, ðosu->perfmon_state.lock) {
if (ethosu->perfmon_state.active || ethosu->global_perfmon) {
ethosu_perfmon_put(perfmon);
return -EBUSY;
}
ethosu->global_perfmon = perfmon;
}
return 0;
}