// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2021, The Linux Foundation. All rights reserved.
 * Copyright (c) 2023-2024 Qualcomm Innovation Center, Inc. All rights reserved.
 */

#include <linux/sched.h>
#include <linux/sched/clock.h>
#include <linux/slab.h>
#include <linux/spinlock.h>

#include "kgsl_device.h"
#include "kgsl_eventlog.h"
#include "kgsl_snapshot.h"
#include "kgsl_util.h"

#define EVENTLOG_SIZE (SZ_64K + SZ_32K)
#define MAGIC 0xabbaabba
#define LOG_FENCE_NAME_LEN 74

#define KGSL_SNAPSHOT_EVENTLOG_TYPE 0x1
#define KGSL_SNAPSHOT_EVENTLOG_VERSION 0x0

/*
 * This an internal event used to skip empty space at the bottom of the
 * ringbuffer
 */

#define LOG_SKIP 1
#define LOG_FIRE_EVENT 2
#define LOG_CMDBATCH_SUBMITTED_EVENT 3
#define LOG_CMDBATCH_RETIRED_EVENT 4
#define LOG_SYNCPOINT_FENCE_EVENT 5
#define LOG_SYNCPOINT_FENCE_EXPIRE_EVENT 6
#define LOG_TIMELINE_FENCE_ALLOC_EVENT 7
#define LOG_TIMELINE_FENCE_RELEASE_EVENT 8

static spinlock_t lock;
static void *kgsl_eventlog;
static int eventlog_wptr;

struct kgsl_log_header {
	/** @magic: Magic value to identify header */
	u32 magic;
	/** @pid: : PID of the process */
	int pid;
	/** @time: System time in nanoseconds */
	u64 time;
	/** @event: bits[0:15] specify the event ID. bits[16:31] specify event version */
	u32 event;
	/** @size: Size of the event data in bytes */
	u32 size;
};

/* Add a marker to skip the rest of the eventlog and start over fresh */
static void add_skip_header(u32 offset)
{
	struct kgsl_log_header *header = kgsl_eventlog + offset;

	header->magic = MAGIC;
	header->time = local_clock();
	header->pid = 0;
	header->event = FIELD_PREP(GENMASK(15, 0), LOG_SKIP);
	header->size = EVENTLOG_SIZE - sizeof(*header) - offset;
}

static void *kgsl_eventlog_alloc(u16 eventid, u32 size)
{
	struct kgsl_log_header *header;
	u32 datasize = size + sizeof(*header);
	unsigned long flags;
	void *data;

	if (!kgsl_eventlog)
		return NULL;

	spin_lock_irqsave(&lock, flags);
	if (eventlog_wptr + datasize > (EVENTLOG_SIZE - sizeof(*header))) {
		add_skip_header(eventlog_wptr);
		eventlog_wptr = datasize;
		data = kgsl_eventlog;
	} else {
		data = kgsl_eventlog + eventlog_wptr;
		eventlog_wptr += datasize;
	}
	spin_unlock_irqrestore(&lock, flags);

	header = data;

	header->magic = MAGIC;
	header->time = local_clock();
	header->pid = current->pid;
	header->event = FIELD_PREP(GENMASK(15, 0), eventid);
	header->size = size;

	return data + sizeof(*header);
}

void kgsl_eventlog_init(void)
{
	kgsl_eventlog = kzalloc(EVENTLOG_SIZE, GFP_KERNEL);
	eventlog_wptr = 0;

	spin_lock_init(&lock);

	kgsl_add_to_minidump("KGSL_EVENTLOG", (u64) kgsl_eventlog,
				__pa(kgsl_eventlog), EVENTLOG_SIZE);
}

void kgsl_eventlog_exit(void)
{
	kgsl_remove_from_minidump("KGSL_EVENTLOG", (u64) kgsl_eventlog,
				__pa(kgsl_eventlog), EVENTLOG_SIZE);

	kfree(kgsl_eventlog);
	kgsl_eventlog = NULL;
	eventlog_wptr = 0;
}

void log_kgsl_fire_event(u32 id, u32 ts, u32 type, u32 age)
{
	struct {
		u32 id;
		u32 ts;
		u32 type;
		u32 age;
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_FIRE_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	entry->ts = ts;
	entry->type = type;
	entry->age = age;
}

void log_kgsl_cmdbatch_submitted_event(u32 id, u32 ts, u32 prio, u64 flags)
{
	struct {
		u32 id;
		u32 ts;
		u32 prio;
		u64 flags;
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_CMDBATCH_SUBMITTED_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	entry->ts = ts;
	entry->prio = prio;
	entry->flags = flags;
}

void log_kgsl_cmdbatch_retired_event(u32 id, u32 ts, u32 prio, u64 flags,
		u64 start, u64 retire)
{
	struct {
		u32 id;
		u32 ts;
		u32 prio;
		u64 flags;
		u64 start;
		u64 retire;
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_CMDBATCH_RETIRED_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	entry->ts = ts;
	entry->prio = prio;
	entry->flags = flags;
	entry->start = start;
	entry->retire = retire;
}

void log_kgsl_syncpoint_fence_event(u32 id, char *fence_name)
{
	struct {
		u32 id;
		char name[LOG_FENCE_NAME_LEN];
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_SYNCPOINT_FENCE_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	memset(entry->name, 0, sizeof(entry->name));
	strscpy(entry->name, fence_name, sizeof(entry->name));
}

void log_kgsl_syncpoint_fence_expire_event(u32 id, char *fence_name)
{
	struct {
		u32 id;
		char name[LOG_FENCE_NAME_LEN];
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_SYNCPOINT_FENCE_EXPIRE_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	memset(entry->name, 0, sizeof(entry->name));
	strscpy(entry->name, fence_name, sizeof(entry->name));
}

void log_kgsl_timeline_fence_alloc_event(u32 id, u64 seqno)
{
	struct {
		u32 id;
		u64 seqno;
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_TIMELINE_FENCE_ALLOC_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	entry->seqno = seqno;
}

void log_kgsl_timeline_fence_release_event(u32 id, u64 seqno)
{
	struct {
		u32 id;
		u64 seqno;
	} *entry;

	entry = kgsl_eventlog_alloc(LOG_TIMELINE_FENCE_RELEASE_EVENT, sizeof(*entry));
	if (!entry)
		return;

	entry->id = id;
	entry->seqno = seqno;
}

size_t kgsl_snapshot_eventlog_buffer(struct kgsl_device *device,
		u8 *buf, size_t remain, void *priv)
{
	struct kgsl_snapshot_eventlog *hdr =
		(struct kgsl_snapshot_eventlog *)buf;
	u32 *data = (u32 *)(buf + sizeof(*hdr));

	if (!kgsl_eventlog)
		return 0;

	if (remain < EVENTLOG_SIZE + sizeof(*hdr)) {
		dev_err(device->dev,
			"snapshot: Not enough memory for eventlog\n");
		return 0;
	}

	hdr->size = EVENTLOG_SIZE;
	hdr->type = KGSL_SNAPSHOT_EVENTLOG_TYPE;
	hdr->version = KGSL_SNAPSHOT_EVENTLOG_VERSION;
	memcpy(data, kgsl_eventlog, EVENTLOG_SIZE);

	return EVENTLOG_SIZE + sizeof(*hdr);
}