/*
 * Copyright © 2014 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.
 *
 * Authors:
 *    Neil Roberts <neil@linux.intel.com>
 *
 */

/** @file glx-context-flush-control.c
 *
 * Tests the GLX_ARB_context_flush_control extension. It takes the
 * following steps using two threads. The threads are only used so it
 * can operate on another context without having to rebind it. The
 * threads are run lock-step so that each step is run sequentially.
 *
 * Thread 1: Make a flushless context A
 * Thread 1: Make a flushy context B, shared with A
 * Thread 1: Make a flushy context C, shared with A
 * Thread 1: Bind context A
 * Thread 2: Bind context C
 * Thread 1: Make a renderbuffer.
 * Thread 1: glClear() it to green.
 * Thread 1: glFinish()
 * Thread 1: glClear() it to red.
 * Thread 2: Do a glReadPixels()
 *
 * (At this point the GL implementation is allowed to have finished
 * the clear to red but it probably won't have. If the read pixels
 * returns green here then it's not a failure but the test won't work
 * so it will report PIGLIT_SKIP)
 *
 * Thread 1: Bind context C
 * Thread 1: sleep(.5)
 * Thread 2: Make sure glReadPixels() is still green, otherwise fail.
 *
 * All of the steps are then run again but this time context A is made
 * flushy and the last step ensures that the pixel becomes red instead
 * of green. If it did become red then the GL successfully made a
 * flush when context A was released.
 *
 * The test also verifies that calling glGetIntegerv with
 * GL_CONTEXT_RELEASE_BEHAVIOR returns the expected value when setting
 * the attribute to none and flush and also when the attribute is left
 * out entirely.
 */

#include <unistd.h>

#include "piglit-util-gl.h"
#include "piglit-glx-util.h"
#include "pthread.h"

#ifndef GLX_ARB_context_flush_control
#define GLX_CONTEXT_RELEASE_BEHAVIOR_ARB 0x2097
#define GLX_CONTEXT_RELEASE_BEHAVIOR_NONE_ARB 0
#define GLX_CONTEXT_RELEASE_BEHAVIOR_FLUSH_ARB 0x2098
#endif

#ifndef GL_CONTEXT_RELEASE_BEHAVIOR
#define GL_CONTEXT_RELEASE_BEHAVIOR 0x82FB
#define GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH 0x82FC
#endif

static PFNGLXCREATECONTEXTATTRIBSARBPROC CreateContextAttribs = NULL;

enum release_behavior {
	RB_NONE,
	RB_FLUSH,
	RB_NOT_SPECIFIED
};

struct window {
	GLXFBConfig config;
	XVisualInfo *xvi;
	Window window;
	GLXWindow glx_window;
};

struct thread_data {
	Display *display;
	struct window *window;
	GLXContext context;

	pthread_mutex_t mutex;
	pthread_cond_t cond;

	bool quit;
	void (* running_func)(struct thread_data *data);

	GLuint fbo;

	enum piglit_result result;
};

static float red[] = { 1.0f, 0.0f, 0.0f };
static float green[] = { 0.0f, 1.0f, 0.0f };

GLXContext
create_context(Display *display,
	       struct window *window,
	       GLXContext share_ctx,
	       enum release_behavior release_behavior)
{
	GLint actual_release_behavior;
	GLXContext ctx;
	int ctx_attribs[7] = {
		GLX_CONTEXT_MAJOR_VERSION_ARB, 1,
		GLX_CONTEXT_MINOR_VERSION_ARB, 5,
		GLX_CONTEXT_RELEASE_BEHAVIOR_ARB,
		release_behavior,
		0
	};

	switch (release_behavior) {
	case RB_NONE:
		ctx_attribs[5] = GLX_CONTEXT_RELEASE_BEHAVIOR_NONE_ARB;
		break;
	case RB_FLUSH:
		ctx_attribs[5] = GLX_CONTEXT_RELEASE_BEHAVIOR_FLUSH_ARB;
		break;
	case RB_NOT_SPECIFIED:
		ctx_attribs[4] = 0;
		break;
	}

	ctx = CreateContextAttribs(display, window->config,
				   share_ctx, True,
				   ctx_attribs);

	assert(ctx);

	glXMakeCurrent(display, window->glx_window, ctx);

	piglit_dispatch_default_init(PIGLIT_DISPATCH_GL);

	piglit_require_extension("GL_KHR_context_flush_control");

	glGetIntegerv(GL_CONTEXT_RELEASE_BEHAVIOR, &actual_release_behavior);

	switch (release_behavior) {
	case RB_FLUSH:
	case RB_NOT_SPECIFIED:
		assert(actual_release_behavior ==
		       GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH);
		       break;
	case RB_NONE:
		assert(actual_release_behavior == GL_NONE);
		       break;
	default:
		assert(0);
	}

	return ctx;
}

static void
create_window(Display *display,
	      struct window *window)
{
	window->xvi = piglit_get_glx_visual(display);
	window->config =
		piglit_glx_get_fbconfig_for_visinfo(display, window->xvi);

	window->window = piglit_get_glx_window(display, window->xvi);
	window->glx_window = glXCreateWindow(display, window->config,
					     window->window, NULL);
}

static void *
thread_func(void *user_data)
{
	struct thread_data *data = user_data;
	bool quit;
	void (* running_func)(struct thread_data *data);

	while (true) {
		/* Wait for something to do */
		pthread_mutex_lock(&data->mutex);
		while (!data->quit && data->running_func == NULL)
			pthread_cond_wait(&data->cond, &data->mutex);
		quit = data->quit;
		running_func = data->running_func;
		pthread_mutex_unlock(&data->mutex);

		if (quit)
			break;

		running_func(data);

		pthread_mutex_lock(&data->mutex);
		data->running_func = NULL;
		pthread_cond_signal(&data->cond);
		pthread_mutex_unlock(&data->mutex);
	}

	return NULL;
}

static void
run_in_thread(struct thread_data *data,
	      void (* func)(struct thread_data *data))
{
	pthread_mutex_lock(&data->mutex);

	/* Tell the thread about the function */
	data->running_func = func;
	pthread_cond_signal(&data->cond);

	/* Wait for it to complete */
	do
		pthread_cond_wait(&data->cond, &data->mutex);
	while (data->running_func);

	pthread_mutex_unlock(&data->mutex);

	if (data->result != PIGLIT_PASS)
		piglit_report_result(data->result);
}

static void
bind_context(struct thread_data *data)
{
	glXMakeCurrent(data->display, data->window->glx_window, data->context);
}

static void
unbind_context(struct thread_data *data)
{
	glXMakeCurrent(data->display, None, NULL);
}

static void
check_green(struct thread_data *data)
{
	glBindFramebuffer(GL_FRAMEBUFFER, data->fbo);

	/* At this point the main thread has flushed a clear to green
	 * and queued a clear to red without flushing. It would be
	 * valid for the framebuffer to be red here but in that case
	 * the test won't work so we will skip the test */
	if (!piglit_probe_pixel_rgb_silent(0, 0, green, NULL)) {
		printf("Either the clear to green command was not completed "
		       "or the clear to red command was flushed too early so "
		       "the test will be skipped\n");
		data->result = PIGLIT_SKIP;
	}

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

static void
check_still_green(struct thread_data *data)
{
	float probe[4];

	glBindFramebuffer(GL_FRAMEBUFFER, data->fbo);

	/* The pixel should still be green even though the main thread
	 * has released the original context because it shouldn't
	 * cause a flush */
	if (!piglit_probe_pixel_rgb_silent(0, 0, green, NULL)) {
		if (piglit_probe_pixel_rgb_silent(0, 0, red, probe)) {
			printf("The renderbuffer contains a red pixel which "
			       "means that releasing the first context has "
			       "caused a flush.\n");
		} else {
			printf("Expected green\n"
			       "Observed: %f %f %f\n",
			       probe[0], probe[1], probe[2]);
		}

		data->result = PIGLIT_FAIL;
	}

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

static void
check_changed_to_red(struct thread_data *data)
{
	float probe[4];

	glBindFramebuffer(GL_FRAMEBUFFER, data->fbo);

	/* Releasing the original context should have caused a flush
	 * so the framebuffer should have become red */
	if (!piglit_probe_pixel_rgb_silent(0, 0, red, NULL)) {
		if (piglit_probe_pixel_rgb_silent(0, 0, green, probe)) {
			printf("The renderbuffer contains a green pixel which "
			       "means that releasing the first context has not "
			       "caused a flush.\n");
		} else {
			printf("Expected red\n"
			       "Observed: %f %f %f\n",
			       probe[0], probe[1], probe[2]);
		}

		data->result = PIGLIT_FAIL;
	}

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

static void
do_test_flush(Display *display,
	      struct window *window,
	      GLXContext context_b,
	      struct thread_data *thread_data,
	      enum release_behavior release_behavior)
{
	GLenum status;
	GLuint fbo, rb;

	glGenRenderbuffers(1, &rb);
	glBindRenderbuffer(GL_RENDERBUFFER, rb);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB, 1, 1);

	glGenFramebuffers(1, &fbo);
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER,
				  GL_COLOR_ATTACHMENT0,
				  GL_RENDERBUFFER,
				  rb);

	status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if (status != GL_FRAMEBUFFER_COMPLETE) {
		printf("failed to create a 1x1 GL_RGB renderbuffer\n");
		piglit_report_result(PIGLIT_SKIP);
	}

	thread_data->fbo = fbo;

	/* Clear the framebuffer to green */
	glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	/* Make sure the color actually hits the framebuffer */
	glFinish();

	/* Post a command to clear it to red without flushing */
	glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	run_in_thread(thread_data, check_green);

	/* Switch to the other context. This shouldn't cause a flush
	 * if the release behavior is RB_NONE */
	glXMakeCurrent(display, window->glx_window, context_b);

	/* Give the GPU some time to finish rendering */
	usleep(500000);

	if (release_behavior == RB_NONE) {
		/* Verify that it didn't cause a flush */
		run_in_thread(thread_data, check_still_green);
	} else {
		/* Make sure it did cause a flush */
		run_in_thread(thread_data, check_changed_to_red);
	}

	glBindRenderbuffer(GL_RENDERBUFFER, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glDeleteRenderbuffers(1, &rb);
	glDeleteFramebuffers(1, &fbo);
}

static void
test_flush(Display *display,
	   struct window *window,
	   enum release_behavior release_behavior)
{
	GLXContext context_a, context_b, context_c;
	struct thread_data thread_data;
	pthread_t thread;

	/* Create three contexts with each of the three possible
	 * values for the release behavior. This also verifies that
	 * the GL extension returns the right value for each attribute
	 * value from glGetIntegerv. We only need one context without
	 * flushing and the value for the others doesn't really
	 * matter */
	context_a = create_context(display, window, NULL, release_behavior);
	piglit_require_extension("GL_EXT_framebuffer_object");
	context_b = create_context(display, window, context_a, RB_FLUSH);
	piglit_require_extension("GL_EXT_framebuffer_object");
	context_c = create_context(display, window,
				   context_a, RB_NOT_SPECIFIED);
	piglit_require_extension("GL_EXT_framebuffer_object");

	thread_data.display = display;
	thread_data.window = window;
	thread_data.context = context_c;

	pthread_mutex_init(&thread_data.mutex, NULL);
	pthread_cond_init(&thread_data.cond, NULL);

	thread_data.quit = false;
	thread_data.running_func = NULL;
	thread_data.result = PIGLIT_PASS;

	pthread_create(&thread, NULL, thread_func, &thread_data);

	glXMakeCurrent(display, window->glx_window, context_a);

	run_in_thread(&thread_data, bind_context);

	do_test_flush(display, window, context_b,
		      &thread_data, release_behavior);

	run_in_thread(&thread_data, unbind_context);

	pthread_mutex_lock(&thread_data.mutex);
	thread_data.quit = true;
	pthread_cond_signal(&thread_data.cond);
	pthread_mutex_unlock(&thread_data.mutex);

	pthread_join(thread, NULL);

	pthread_cond_destroy(&thread_data.cond);
	pthread_mutex_destroy(&thread_data.mutex);

	glXDestroyContext(display, context_c);
	glXDestroyContext(display, context_b);
	glXDestroyContext(display, context_a);
}

static void
destroy_window(Display *display,
	       struct window *window)
{
	glXDestroyWindow(display, window->glx_window);
	XDestroyWindow(display, window->window);
}

int
main(int argc, char **argv)
{
	Display *display;
	struct window window;
	bool pass = true;

	for (int i = 1; i < argc; i++) {
		if (!strcmp(argv[i], "-auto"))
			piglit_automatic = 1;
		else
			fprintf(stderr, "Unknown option: %s\n", argv[i]);
	}

	display = piglit_get_glx_display();

	piglit_require_glx_extension(display, "GLX_ARB_get_proc_address");
	piglit_require_glx_extension(display, "GLX_ARB_create_context");
	piglit_require_glx_extension(display, "GLX_ARB_context_flush_control");

	CreateContextAttribs = (PFNGLXCREATECONTEXTATTRIBSARBPROC)
		glXGetProcAddressARB((GLubyte *) "glXCreateContextAttribsARB");

	create_window(display, &window);

	test_flush(display, &window, RB_NONE);
	test_flush(display, &window, RB_FLUSH);

	destroy_window(display, &window);

	piglit_report_result(pass ? PIGLIT_PASS : PIGLIT_FAIL);

	return 0;
}
