/*
 * Copyright © 2020 Intel Corporation
 *
 * 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 (including the next
 * paragraph) 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 AUTHORS OR COPYRIGHT HOLDERS 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.
 *
 * Author:
 *  Swati Sharma <swati2.sharma@intel.com>
 */

/**
 * TEST: kms cdclk
 * Category: Display
 * Description: Test cdclk features : crawling and squashing
 * Driver requirement: i915, xe
 * Functionality: kms_core
 * Mega feature: General Display Features
 * Test category: functionality test
 */

#include "igt.h"

/**
 * SUBTEST: mode-transition-all-outputs
 * Description: Mode transition (low to high) test to validate cdclk frequency
 *              change by simultaneous modesets on all pipes with valid outputs.
 *
 * SUBTEST: mode-transition
 * Description: Mode transition (low to high) test to validate cdclk frequency change.
 *
 * SUBTEST: plane-scaling
 * Description: Plane scaling test to validate cdclk frequency change.
 */

IGT_TEST_DESCRIPTION("Test cdclk features : crawling and squashing");

#define HDISPLAY_4K	3840
#define VDISPLAY_4K	2160
#define VREFRESH	60
#define MAX_CDCLK_4K	307200

/* Test flags */
enum {
	TEST_PLANESCALING = 1 << 0,
	TEST_MODETRANSITION = 1 << 1,
};

typedef struct {
	int drm_fd;
	int debugfs_fd;
	uint32_t devid;
	igt_display_t display;
} data_t;

static bool hardware_supported(data_t *data)
{
        if (intel_display_ver(data->devid) >= 13)
		return true;

	return false;
}

static int get_current_cdclk_freq(int debugfs_fd)
{
	int cdclk_freq_current;
	char buf[1024];
	char *start_loc;
	int res;

	/*
	 * Display specific clock frequency info is moved to i915_cdclk_info,
	 * On older kernels if this debugfs is not found, fallback to read from
	 * i915_frequency_info.
	 *
	 * FIXME: As of now, XE debugfs is still using i915 namespace, once the
	 * Kernel changes are landed, update this to use the XE specific debugfs.
	 */
	res = igt_debugfs_simple_read(debugfs_fd, "i915_cdclk_info",
				      buf, sizeof(buf));
	if (res <= 0)
		res = igt_debugfs_simple_read(debugfs_fd, "i915_frequency_info",
					      buf, sizeof(buf));
	igt_require(res > 0);

	igt_assert(start_loc = strstr(buf, "Current CD clock frequency: "));
	igt_assert_eq(sscanf(start_loc, "Current CD clock frequency: %d", &cdclk_freq_current), 1);

	return cdclk_freq_current;
}

static __u64 get_mode_data_rate(drmModeModeInfo *mode)
{
	__u64 data_rate = (__u64)mode->hdisplay * (__u64)mode->vdisplay * (__u64)mode->vrefresh;
	return data_rate;
}

static drmModeModeInfo *get_highres_mode(igt_output_t *output)
{
	drmModeModeInfo *highest_mode = NULL;
	drmModeConnector *connector = output->config.connector;
	int j;

	for (j = 0; j < connector->count_modes; j++) {
		if (connector->modes[j].vdisplay == VDISPLAY_4K &&
		    connector->modes[j].hdisplay == HDISPLAY_4K &&
		    connector->modes[j].vrefresh == VREFRESH) {
			highest_mode = &connector->modes[j];
			break;
		}
	}

	return highest_mode;
}

static drmModeModeInfo *get_lowres_mode(igt_output_t *output)
{
	drmModeModeInfo *lowest_mode = NULL;
	drmModeConnector *connector = output->config.connector;
	int j;

	for (j = 0; j < connector->count_modes; j++) {
		if (!lowest_mode) {
			lowest_mode = &connector->modes[j];
		} else if (connector->modes[j].vdisplay && connector->modes[j].hdisplay) {
			__u64 lowest_data_rate = get_mode_data_rate(lowest_mode);
			__u64 data_rate = get_mode_data_rate(&output->config.connector->modes[j]);

			if (lowest_data_rate > data_rate)
				lowest_mode = &connector->modes[j];
		}
	}

	return lowest_mode;
}

static void do_cleanup_display(igt_display_t *dpy)
{
	enum pipe pipe;
	igt_output_t *output;
	igt_plane_t *plane;

	for_each_pipe(dpy, pipe)
		for_each_plane_on_pipe(dpy, pipe, plane)
			igt_plane_set_fb(plane, NULL);

	for_each_connected_output(dpy, output)
		igt_output_set_pipe(output, PIPE_NONE);

	igt_display_commit2(dpy, dpy->is_atomic ? COMMIT_ATOMIC : COMMIT_LEGACY);
}

static void test_plane_scaling(data_t *data, enum pipe pipe, igt_output_t *output)
{
	igt_display_t *display = &data->display;
	int debugfs_fd = data->debugfs_fd;
	int cdclk_ref, cdclk_new;
	struct igt_fb fb;
	igt_plane_t *primary;
	drmModeModeInfo *mode;
	int scaling = 50;
	int ret;
	bool test_complete = false;

	while (!test_complete) {
		do_cleanup_display(display);
		igt_display_reset(display);

		igt_output_set_pipe(output, pipe);
		mode = get_highres_mode(output);
		igt_require(mode != NULL);
		igt_output_override_mode(output, mode);

		primary = igt_output_get_plane_type(output, DRM_PLANE_TYPE_PRIMARY);

		igt_create_color_pattern_fb(display->drm_fd,
					    mode->hdisplay, mode->vdisplay,
					    DRM_FORMAT_XRGB8888,
					    DRM_FORMAT_MOD_LINEAR,
					    0.0, 0.0, 0.0, &fb);
		igt_plane_set_fb(primary, &fb);

		/* downscaling */
		igt_plane_set_size(primary, ((fb.width * scaling) / 100), ((fb.height * scaling) / 100));
		cdclk_ref = get_current_cdclk_freq(debugfs_fd);
		ret = igt_display_try_commit_atomic(display, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
		if (ret != -EINVAL) {
			igt_display_commit_atomic(display, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
			cdclk_new = get_current_cdclk_freq(debugfs_fd);
			igt_info("CD clock frequency %d -> %d\n", cdclk_ref, cdclk_new);

			/* cdclk should bump */
			igt_assert_lt(cdclk_ref, cdclk_new);

			test_complete = true;
		}

		scaling += 5;

		/* cleanup */
		do_cleanup_display(display);
		igt_remove_fb(display->drm_fd, &fb);
	}
}

static void test_mode_transition(data_t *data, enum pipe pipe, igt_output_t *output)
{
	igt_display_t *display = &data->display;
	int debugfs_fd = data->debugfs_fd;
	int cdclk_ref, cdclk_new;
	struct igt_fb fb;
	igt_plane_t *primary;
	drmModeModeInfo *mode_hi, *mode_lo, *mode;

	do_cleanup_display(display);
	igt_display_reset(display);

	igt_output_set_pipe(output, pipe);
	mode = igt_output_get_mode(output);
	mode_lo = get_lowres_mode(output);
	mode_hi = get_highres_mode(output);
	igt_require(mode_hi != NULL);

	igt_skip_on_f(mode_hi->hdisplay == mode_lo->hdisplay && mode_hi->vdisplay == mode_lo->vdisplay,
		      "Highest and lowest mode resolutions are same; no transition\n");

	primary = igt_output_get_plane_type(output, DRM_PLANE_TYPE_PRIMARY);

	igt_create_color_pattern_fb(display->drm_fd,
				    mode->hdisplay, mode->vdisplay,
				    DRM_FORMAT_XRGB8888,
				    DRM_FORMAT_MOD_LINEAR,
				    0.0, 0.0, 0.0, &fb);

	/* switch to lower resolution */
	igt_output_override_mode(output, mode_lo);
	igt_plane_set_fb(primary, &fb);
	igt_display_commit_atomic(display, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
	cdclk_ref = get_current_cdclk_freq(debugfs_fd);

	/* switch to higher resolution */
	igt_output_override_mode(output, mode_hi);
	igt_plane_set_fb(primary, &fb);
	igt_display_commit_atomic(display, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
	cdclk_new = get_current_cdclk_freq(debugfs_fd);
	igt_info("CD clock frequency %d -> %d\n", cdclk_ref, cdclk_new);

	/* cdclk should bump */
	if (cdclk_new != MAX_CDCLK_4K)
		igt_assert_lt(cdclk_ref, cdclk_new);

	/* cleanup */
	do_cleanup_display(display);
	igt_remove_fb(display->drm_fd, &fb);
}

static void test_mode_transition_on_all_outputs(data_t *data)
{
	igt_display_t *display = &data->display;
	int debugfs_fd = data->debugfs_fd;
	drmModeModeInfo *mode, *mode_hi, *mode_lo;
	igt_output_t *output;
	int valid_outputs = 0;
	int cdclk_ref, cdclk_new;
	uint16_t width = 0, height = 0;
	struct igt_fb fb;
	igt_pipe_t *pipe;
	igt_plane_t *plane;
	int i = 0, j = 0;

	do_cleanup_display(display);
	igt_display_reset(display);

	for_each_connected_output(&data->display, output)
		valid_outputs++;

	i = 0;
	for_each_connected_output(display, output) {
		mode = igt_output_get_mode(output);
		igt_assert(mode);

		width = max(width, mode->hdisplay);
		height = max(height, mode->vdisplay);

		mode_hi = get_highres_mode(output);
		igt_require(mode_hi != NULL);

		igt_output_set_pipe(output, i);
		igt_output_override_mode(output, mode_hi);
		i++;
	}
	igt_require(intel_pipe_output_combo_valid(display));
	igt_display_reset(display);

	igt_create_pattern_fb(data->drm_fd, width, height, DRM_FORMAT_XRGB8888,
			      DRM_FORMAT_MOD_LINEAR, &fb);
	i = 0;
	for_each_connected_output(display, output) {
		pipe = &display->pipes[i];
		plane = igt_pipe_get_plane_type(pipe, DRM_PLANE_TYPE_PRIMARY);

		mode = NULL;

		igt_output_set_pipe(output, i);
		mode = igt_output_get_mode(output);
		igt_assert(mode);

		mode_lo = get_lowres_mode(output);

		igt_output_override_mode(output, mode_lo);
		igt_plane_set_fb(plane, &fb);
		igt_fb_set_size(&fb, plane, mode_lo->hdisplay, mode_lo->vdisplay);
		igt_plane_set_size(plane, mode_lo->hdisplay, mode_lo->vdisplay);
		i++;
	}

	igt_display_commit2(display, COMMIT_ATOMIC);
	cdclk_ref = get_current_cdclk_freq(debugfs_fd);

	j = 0;
	for_each_connected_output(display, output) {
		pipe = &display->pipes[j];
		plane = igt_pipe_get_plane_type(pipe, DRM_PLANE_TYPE_PRIMARY);

		mode = NULL;

		igt_output_set_pipe(output, j);
		mode = igt_output_get_mode(output);
		igt_assert(mode);

		mode_hi = get_highres_mode(output);
		igt_require(mode_hi != NULL);

		igt_output_override_mode(output, mode_hi);
		igt_plane_set_fb(plane, &fb);
		igt_fb_set_size(&fb, plane, mode_hi->hdisplay, mode_hi->vdisplay);
		igt_plane_set_size(plane, mode_hi->hdisplay, mode_hi->vdisplay);
		j++;
	}

	igt_display_commit2(display, COMMIT_ATOMIC);
	cdclk_new = get_current_cdclk_freq(debugfs_fd);
	igt_info("CD clock frequency %d -> %d\n", cdclk_ref, cdclk_new);

	/* cdclk should bump */
	igt_assert_lt(cdclk_ref, cdclk_new);

	igt_plane_set_fb(plane, NULL);
	do_cleanup_display(display);
	igt_remove_fb(data->drm_fd, &fb);
}

static void run_cdclk_test(data_t *data, uint32_t flags)
{
	igt_display_t *display = &data->display;
	igt_output_t *output;
	enum pipe pipe;

	for_each_pipe_with_valid_output(display, pipe, output) {
		igt_output_set_pipe(output, pipe);
		if (!intel_pipe_output_combo_valid(display)) {
			igt_output_set_pipe(output, PIPE_NONE);
			continue;
		}

		igt_dynamic_f("pipe-%s-%s", kmstest_pipe_name(pipe), output->name) {
			if (flags & TEST_PLANESCALING)
				test_plane_scaling(data, pipe, output);
			if (flags & TEST_MODETRANSITION)
				test_mode_transition(data, pipe, output);
		}
	}
}

igt_main
{
	data_t data = {};

	igt_fixture {
		data.drm_fd = drm_open_driver_master(DRIVER_INTEL | DRIVER_XE);
		igt_require(data.drm_fd >= 0);
		data.debugfs_fd = igt_debugfs_dir(data.drm_fd);
		igt_require(data.debugfs_fd);
		kmstest_set_vt_graphics_mode();
		data.devid = intel_get_drm_devid(data.drm_fd);
		igt_require_f(hardware_supported(&data),
			      "Hardware doesn't support crawling/squashing.\n");
		igt_display_require(&data.display, data.drm_fd);
		igt_display_require_output(&data.display);

		/* Wa_15015413771: Only single possible cdclk value in XE2_HPD */
		igt_require(!IS_BATTLEMAGE(data.devid));
	}

	igt_describe("Plane scaling test to validate cdclk frequency change.");
	igt_subtest_with_dynamic("plane-scaling")
		run_cdclk_test(&data, TEST_PLANESCALING);
	igt_describe("Mode transition (low to high) test to validate cdclk frequency change.");
	igt_subtest_with_dynamic("mode-transition")
		run_cdclk_test(&data, TEST_MODETRANSITION);

	igt_describe("Mode transition (low to high) test to validate cdclk frequency change "
		     "by simultaneous modesets on all pipes with valid outputs.");
	igt_subtest("mode-transition-all-outputs")
		test_mode_transition_on_all_outputs(&data);

	igt_fixture {
		igt_display_fini(&data.display);
		drm_close_driver(data.drm_fd);
	}
}
