/*
 * Copyright (c) 2011-2014 Wind River Systems, Inc.
 * Copyright (c) 2020 Stephanos Ioannidis <root@stephanos.io>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/*
 * @file
 * @brief pi computation portion of FPU sharing test
 *
 * @ingroup kernel_fpsharing_tests
 *
 * This module is used for the FPU sharing test, and supplements the basic
 * load/store test by incorporating two additional threads that utilize the
 * floating point unit.
 *
 * Testing utilizes a pair of tasks that independently compute pi. The lower
 * priority task is regularly preempted by the higher priority task, thereby
 * testing whether floating point context information is properly preserved.
 *
 * The following formula is used to compute pi:
 *
 *     pi = 4 * (1 - 1/3 + 1/5 - 1/7 + 1/9 - ... )
 *
 * This series converges to pi very slowly. For example, performing 50,000
 * iterations results in an accuracy of 3 decimal places.
 *
 * A reference value of pi is computed once at the start of the test. All
 * subsequent computations must produce the same value, otherwise an error
 * has occurred.
 */

#include <ztest.h>

#include "float_context.h"
#include "test_common.h"

/*
 * PI_NUM_ITERATIONS: This macro is defined in the project's Makefile and
 * is configurable from the command line.
 */
static float reference_pi = 0.0f;

/*
 * Test counters are "volatile" because GCC wasn't properly updating
 * calc_pi_low_count properly when calculate_pi_low() contained a "return"
 * in its error handling logic -- the value was incremented in a register,
 * but never written back to memory. (Seems to be a compiler bug!)
 */
static volatile unsigned int calc_pi_low_count;
static volatile unsigned int calc_pi_high_count;

/* Indicates that the load/store test exited */
static bool test_exited;

/* Semaphore for signaling end of test */
static K_SEM_DEFINE(test_exit_sem, 0, 1);

/**
 * @brief Entry point for the low priority pi compute task
 *
 * @ingroup kernel_fpsharing_tests
 */
static void calculate_pi_low(void)
{
	volatile float pi; /* volatile to avoid optimizing out of loop */
	float divisor = 3.0f;
	float sign = -1.0f;
	unsigned int ix;

	/* Loop until the test finishes, or an error is detected. */
	for (calc_pi_low_count = 0; !test_exited; calc_pi_low_count++) {

		sign = -1.0f;
		pi = 1.0f;
		divisor = 3.0f;

		for (ix = 0; ix < PI_NUM_ITERATIONS; ix++) {
			pi += sign / divisor;
			divisor += 2.0f;
			sign *= -1.0f;
		}

		pi *= 4.0f;

		if (reference_pi == 0.0f) {
			reference_pi = pi;
		} else if (reference_pi != pi) {
			printf("Computed pi %1.6f, reference pi %1.6f\n",
			       pi, reference_pi);
		}

		zassert_equal(reference_pi, pi,
			      "pi computation error");
	}
}

/**
 * @brief Entry point for the high priority pi compute task
 *
 * @ingroup kernel_fpsharing_tests
 */
static void calculate_pi_high(void)
{
	volatile float pi; /* volatile to avoid optimizing out of loop */
	float divisor = 3.0f;
	float sign = -1.0f;
	unsigned int ix;

	/* Run the test until the specified maximum test count is reached */
	for (calc_pi_high_count = 0;
	     calc_pi_high_count <= MAX_TESTS;
	     calc_pi_high_count++) {

		sign = -1.0f;
		pi = 1.0f;
		divisor = 3.0f;

		for (ix = 0; ix < PI_NUM_ITERATIONS; ix++) {
			pi += sign / divisor;
			divisor += 2.0f;
			sign *= -1.0f;
		}

		/*
		 * Relinquish the processor for the remainder of the current
		 * system clock tick, so that lower priority threads get a
		 * chance to run.
		 *
		 * This exercises the ability of the kernel to restore the
		 * FPU state of a low priority thread _and_ the ability of the
		 * kernel to provide a "clean" FPU state to this thread
		 * once the sleep ends.
		 */
		k_sleep(K_MSEC(10));

		pi *= 4.0f;

		if (reference_pi == 0.0f) {
			reference_pi = pi;
		} else if (reference_pi != pi) {
			printf("Computed pi %1.6f, reference pi %1.6f\n",
			       pi, reference_pi);
		}

		zassert_equal(reference_pi, pi,
			      "pi computation error");

		/* Periodically issue progress report */
		if ((calc_pi_high_count % 100) == 50) {
			printf("Pi calculation OK after %u (high) +"
			       " %u (low) tests (computed %1.6lf)\n",
			       calc_pi_high_count, calc_pi_low_count, pi);
		}
	}

	/* Signal end of test */
	test_exited = true;
	k_sem_give(&test_exit_sem);
}

K_THREAD_DEFINE(pi_low, THREAD_STACK_SIZE, calculate_pi_low, NULL, NULL, NULL,
		THREAD_LOW_PRIORITY, THREAD_FP_FLAGS, K_TICKS_FOREVER);

K_THREAD_DEFINE(pi_high, THREAD_STACK_SIZE, calculate_pi_high, NULL, NULL, NULL,
		THREAD_HIGH_PRIORITY, THREAD_FP_FLAGS, K_TICKS_FOREVER);

void test_pi(void)
{
	/* Initialise test states */
	test_exited = false;
	k_sem_reset(&test_exit_sem);

	/* Start test threads */
	k_thread_start(pi_low);
	k_thread_start(pi_high);

	/* Wait for test threads to exit */
	k_sem_take(&test_exit_sem, K_FOREVER);
}
