/*
 * Copyright (c) 2011 Intel Corporation
 * Copyright (c) 2019 Rob Clark <robdclark@gmail.com>
 *
 * 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, sub license,
 * 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 NON-INFRINGEMENT. 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.
 */

#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <math.h>

#include <GLES3/gl3.h>
#include <GLES3/gl3ext.h>

#ifdef HAVE_LIBPNG
#include <png.h>
#endif

#include "common.h"
#include "drm-common.h"

/* A tool for debugging texture layout.  Somewhat inspired by piglit's
 * texelFetch, but with some differences:
 *
 *  - Uses GLES3+ and kms/gbm to reduce dependencies.
 *  - Only samples from FS stage, since this simplifies things and
 *    testing from VS/GS is not important for texture layout
 *  - Encodes the slice and mipmap level as the texture contents;
 *    since this is mostly for making sure we have the slice and
 *    level offsets to put he pixel data where the hw expects to
 *    read it from, the x/y gradient is less useful, but knowing
 *    the slice/level that was actually read helps give better
 *    error messages.  (Once this is working correctly, you can
 *    go off and run texelFetch to make sure you didn't get the
 *    tiling format wrong, etc.)
 *  - Supports all different formats, since the rules for calculate
 *    level/slice offset can differ, or be parameterized differently,
 *    for different formats.
 *  - multiple levels of zoom.. samples same tex coord for NxN screen
 *    coordinates, to more easily see smaller mipmap levels on hidpi
 *    screens.
 *
 * Like texelFetch, it also supports:
 *  - 2D, 3D, 2DArray
 *  - Mipmapping
 *
 * Description of layout on screen from texelFetch:
 *
 * Draws a series of "rectangles" which display each miplevel and array slice,
 * at full size.  They are layed out as follows:
 *
 * miplevel 3 +          +          +          +          +
 *
 * miplevel 2 +-+        +-+        +-+        +-+        +-+
 *            +-+        +-+        +-+        +-+        +-+
 *
 * miplevel 1 +---+      +---+      +---+      +---+      +---+
 *            |   |      |   |      |   |      |   |      |   |
 *            +---+      +---+      +---+      +---+      +---+
 *
 *            +------+   +------+   +------+   +------+   +------+
 * miplevel 0 |      |   |      |   |      |   |      |   |      |
 *            |      |   |      |   |      |   |      |   |      |
 *            +------+   +------+   +------+   +------+   +------+
 *            slice #0   slice #1   slice #2   slice #3   slice #4
 *
 */

static struct egl _egl;
static const struct egl *egl = &_egl;
static const struct gbm *gbm;
static const struct drm *drm;
static int max_error_frames = 5;
static int error_frames;
static int zoom = 1;
static bool full;
static bool stop;
#ifdef HAVE_LIBPNG
static bool png;
#endif
static GLenum target;
static struct size {
	unsigned x, y, z;
} size, minsz, maxsz;
int miplevels;

static bool is_array(GLenum target)
{
	return target == GL_TEXTURE_2D_ARRAY;
}

static int get_ncomp(GLenum ufmt)
{
	switch (ufmt) {
	case GL_RED:
	case GL_RED_INTEGER:
	case GL_DEPTH_COMPONENT:
		return 1;
	case GL_RG:
	case GL_RG_INTEGER:
	case GL_DEPTH_STENCIL:
		return 2;
	case GL_RGB:
	case GL_RGB_INTEGER:
		return 3;
	case GL_RGBA:
	case GL_RGBA_INTEGER:
		return 4;
	default:
		assert(!"bad format");
		return 0;
	}
}

/*
 * Formats Table
 * -------------
 *
 * The worst case (smallest number of bits) to encode slice and level # are
 * the R8 formats, but 4 bits for each is sufficient precision.  To simplify
 * things we standardize[*] how this is encoded:
 *
 *    +----------+--------------+--------------------+
 *    | type     | range used   | GLenum type        |
 *    +----------+--------------+--------------------+
 *    |  SNORM   |  -1.0..1.0   |  GL_BYTE           |
 *    |  UNORM   |   0.0..1.0   |  GL_UNSIGNED_BYTE  |
 *    |  FLOAT   |   0.0..1.0   |  GL_FLOAT          |
 *    |  SINT8   |  -128..127   |  GL_BYTE           |
 *    |  UINT8   |  -128..127   |  GL_UNSIGNED_BYTE  |
 *    |  SINT16  |  -128..127   |  GL_SHORT          |
 *    |  UINT16  |  -128..127   |  GL_UNSIGNED_SHORT |
 *    |  SINT32  |  -128..127   |  GL_INT            |
 *    |  UINT32  |  -128..127   |  GL_UNSIGNED_INT   |
 *    +----------+--------------+--------------------+
 *
 * [*] The "oddball" formats like RGB565 will need an additional level of
 *     "packing" to encode the 8bit slice+level across multiple channels.
 *     Other formats simply replicate the same value across all channels.
 *
 * The shader unpacks the slice/level value and returns them as gl_FragColor
 * red and green channels.
 */
enum type {
	SNORM,
	UNORM,
	FLOAT,
	SINT8,
	UINT8,
	SINT16,
	UINT16,
	SINT32,
	UINT32,
	// TODO add oddballs
};

static int enc_ls(int level, int slice)
{
	return ((level << 3) & 0x78) | (slice & 0x7);
}

static void * encode_BYTE(void *buf, int ncomp, int w, int h, int level, int slice)
{
	int8_t *ptr = buf;
	int8_t val = enc_ls(level, slice) - 127;

	for (int i = 0; i < h; i++) {
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
		val += (i & 1) ? -128 : 128;
	}

	return ptr;
}

static void * encode_UNSIGNED_BYTE(void *buf, int ncomp, int w, int h, int level, int slice)
{
	uint8_t *ptr = buf;
	uint8_t val = enc_ls(level, slice);

	for (int i = 0; i < h; i++) {
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
		val += (i & 1) ? -128 : 128;
	}

	return ptr;
}

static void * encode_FLOAT(void *buf, int ncomp, int w, int h, int level, int slice)
{
	float *ptr = buf;
	int encoded_value = enc_ls(level, slice);
	float original_value = ((float)encoded_value) / 255.0;
	float complement_value = ((float)(encoded_value + 128)) / 255.0;
	float val;

	for (int i = 0; i < h; i++) {
		val = (i & 1) ? complement_value : original_value;
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
	}

	return ptr;
}

static void * encode_SHORT(void *buf, int ncomp, int w, int h, int level, int slice)
{
	int16_t *ptr = buf;
	int16_t val = enc_ls(level, slice) - 127;

	for (int i = 0; i < h; i++) {
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
		val += (i & 1) ? -128 : 128;
	}

	return ptr;
}

static void * encode_UNSIGNED_SHORT(void *buf, int ncomp, int w, int h, int level, int slice)
{
	uint16_t *ptr = buf;
	uint16_t val = enc_ls(level, slice);

	for (int i = 0; i < h; i++) {
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
		val += (i & 1) ? -128 : 128;
	}

	return ptr;
}

static void * encode_INT(void *buf, int ncomp, int w, int h, int level, int slice)
{
	int32_t *ptr = buf;
	int32_t val = enc_ls(level, slice) - 127;

	for (int i = 0; i < h; i++) {
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
		val += (i & 1) ? -128 : 128;
	}
	return ptr;
}

static void * encode_UNSIGNED_INT(void *buf, int ncomp, int w, int h, int level, int slice)
{
	uint32_t *ptr = buf;
	uint32_t val = enc_ls(level, slice);

	for (int i = 0; i < h; i++) {
		for (int j = 0; j < (w*ncomp); j++)
			*(ptr++) = val;
		val += (i & 1) ? -128 : 128;
	}

	return ptr;
}

static struct type_info {
	const char *unpack;
	const char *convert;
	GLenum type;
	void * (*encode)(void *buf, int ncomp, int w, int h, int level, int slice);
} type_info[] = {
#define _TYPE(_name, _unpack, _convert, _type) \
	[_name] = { _unpack, _convert, GL_ ## _type, encode_ ## _type }
/* for the simple types that just encode value in .r channel: */
#define STYPE(_name, _convert, _type) _TYPE(_name, "color.r", _convert, _type)

	STYPE(SNORM,  "(val + 1.0) * 127.0", BYTE),
	STYPE(UNORM,  "val * 255.0",         UNSIGNED_BYTE),
	STYPE(FLOAT,  "val * 255.0",         FLOAT),
	STYPE(SINT8,  "val + 127",           BYTE),
	STYPE(UINT8,  "val",                 UNSIGNED_BYTE),
	STYPE(SINT16, "val + 127",           SHORT),
	STYPE(UINT16, "val",                 UNSIGNED_SHORT),
	STYPE(SINT32, "val + 127",           INT),
	STYPE(UINT32, "val",                 UNSIGNED_INT),
};

static const struct fmt {
	const char *name;
	GLenum ifmt;    /* sized internal format */
	GLenum ufmt;    /* unsized format */
	enum type type;
} fmts[] = {
#define FMT(name, ufmt, t) { #name, GL_##name, ufmt, t }
	FMT(R8,            GL_RED,           UNORM),
	FMT(R8UI,          GL_RED_INTEGER,   UINT8 ),
	FMT(R8I,           GL_RED_INTEGER,   SINT8 ),
	FMT(R16UI,         GL_RED_INTEGER,   UINT16),
	FMT(R16I,          GL_RED_INTEGER,   SINT16),
	FMT(R32UI,         GL_RED_INTEGER,   UINT32),
	FMT(R32I,          GL_RED_INTEGER,   SINT32),
	FMT(RG8,           GL_RG,            UNORM ),
	FMT(RG8UI,         GL_RG_INTEGER,    UINT8 ),
	FMT(RG8I,          GL_RG_INTEGER,    SINT8 ),
	FMT(RG16UI,        GL_RG_INTEGER,    UINT16),
	FMT(RG16I,         GL_RG_INTEGER,    SINT16),
	FMT(RG32UI,        GL_RG_INTEGER,    UINT32),
	FMT(RG32I,         GL_RG_INTEGER,    SINT32),
	FMT(RGB8,          GL_RGB,           UNORM ),
//	FMT(RGB565,        GL_RGB,           UNORM565),
	FMT(RGBA8,         GL_RGBA,          UNORM ),
//	FMT(RGB5_A1,       GL_RGBA,          UINT5551),
//	FMT(RGBA4,         GL_RGBA,          UINT4444),
//	FMT(RGB10_A2,      GL_RGBA,          UINT10A2),
	FMT(RGBA8UI,       GL_RGBA_INTEGER,  UINT8 ),
	FMT(RGBA8I,        GL_RGBA_INTEGER,  SINT8 ),
	FMT(RGBA16UI,      GL_RGBA_INTEGER,  UINT16),
	FMT(RGBA16I,       GL_RGBA_INTEGER,  SINT16),
	FMT(RGBA32I,       GL_RGBA_INTEGER,  SINT32),
	FMT(RGBA32UI,      GL_RGBA_INTEGER,  UINT32),
	/* Not required to be color renderable: */
	FMT(R8_SNORM,       GL_RED,          SNORM ),
	FMT(R16F,           GL_RED,          FLOAT ),
	FMT(R32F,           GL_RED,          FLOAT ),
	FMT(RG8_SNORM,      GL_RG,           SNORM ),
	FMT(RG16F,          GL_RG,           FLOAT ),
	FMT(RG32F,          GL_RG,           FLOAT ),
	FMT(SRGB8,          GL_RGB,          UNORM ),
	FMT(RGB8_SNORM,     GL_RGB,          SNORM ),
	FMT(R11F_G11F_B10F, GL_RGB,          FLOAT ),
	FMT(RGB9_E5,        GL_RGB,          FLOAT ),
	FMT(RGB16F,         GL_RGB,          FLOAT ),
	FMT(RGB32F,         GL_RGB,          FLOAT ),
	FMT(RGB8UI,         GL_RGB_INTEGER,  UINT8 ),
	FMT(RGB8I,          GL_RGB_INTEGER,  SINT8 ),
	FMT(RGB16UI,        GL_RGB_INTEGER,  UINT16),
	FMT(RGB16I,         GL_RGB_INTEGER,  SINT16),
	FMT(RGB32UI,        GL_RGB_INTEGER,  UINT32),
	FMT(RGB32I,         GL_RGB_INTEGER,  SINT32),
	FMT(RGBA8_SNORM,    GL_RGBA,         UNORM ),
	FMT(RGBA16F,        GL_RGBA,         FLOAT ),
	FMT(RGBA32F,        GL_RGBA,         FLOAT ),
	FMT(DEPTH_COMPONENT16,  GL_DEPTH_COMPONENT, UINT16),
	FMT(DEPTH_COMPONENT24,  GL_DEPTH_COMPONENT, UINT32),
	FMT(DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, FLOAT ),
//	FMT(DEPTH24_STENCIL8,   GL_DEPTH_STENCIL,   UINTZ24S8),
//	FMT(DEPTH32F_STENCIL8,  GL_DEPTH_STENCIL,   FLOATZ32UINTX24S8),
};

static const struct fmt *fmt;

/*
 * Shaders:
 */

static const char *get_prefix(void)
{
	switch(fmt->type) {
	case SINT8:
	case SINT16:
	case SINT32:
		return "i";
	case UINT8:
	case UINT16:
	case UINT32:
		return "u";
	default:
		return "";
	}
}

static const char *get_sampler(void)
{
	static char buf[32];
	const char *stype;

	if (target == GL_TEXTURE_2D) {
		stype = "2D";
	} else if (target == GL_TEXTURE_2D_ARRAY) {
		stype = "2DArray";
	} else if (target == GL_TEXTURE_3D) {
		stype = "3D";
	} else {
		assert(!"bad mode!");
	}

	sprintf(buf, "%ssampler%s", get_prefix(), stype);
	return buf;
}

#define IN_POSITION 0
#define IN_TEXCOORD 1
const char *vertex_shader_source =
	"#version 300 es                           \n"
	"in vec4 in_position;                      \n"
	"in vec4 in_texcoord;                      \n"
	"out vec4 v_texcoord;                      \n"
	"void main()                               \n"
	"{                                         \n"
	"	v_texcoord = in_texcoord;          \n"
	"	gl_Position = in_position;         \n"
	"}                                         \n";

const char *fragment_shader_fmt =
	"#version 300 es                                                    \n"
	"#define ivec1 int                                                  \n"
	"#define uvec1 uint                                                 \n"
	"#define vec1 float                                                 \n"
	"precision highp float;                                             \n"
	"precision highp int;                                               \n"
	"in vec4 v_texcoord;                                                \n"
	"uniform highp %1$s tex;                                            \n"
	"out vec4 fragColor;                                                \n"
	"void main()                                                        \n"
	"{                                                                  \n"
	"	int lod = int(v_texcoord.w);                                \n"
	"	%2$svec4 color = texelFetch(tex, ivec%3$d(v_texcoord), lod);\n"
	"	%2$svec1 val = %4$s;                                        \n"
	"	int converted = int(%5$s);                                  \n"
	"	fragColor.rgb = vec3(                                       \n"
	"		float((converted >> 0) & 0x7) /  8.0,               \n"
	"		float((converted >> 3) & 0xf) / 16.0,               \n"
	"		float((converted >> 7) & 0x1) *  0.25                \n"
	"	);                                                          \n"
	"	fragColor.a = 1.0;                                          \n"
	"}                                                                  \n";

static const char *get_fs(void)
{
	static char buf[4096];
	int ncoord = (target == GL_TEXTURE_2D) ? 2 : 3;
	unsigned n;

	n = sprintf(buf, fragment_shader_fmt, get_sampler(),
		get_prefix(), ncoord,
		type_info[fmt->type].unpack,
		type_info[fmt->type].convert);
	assert(n < sizeof(buf) - 1);

	return buf;
}

static void upload_texture(void)
{
	int ncomp = get_ncomp(fmt->ufmt);
	char texbuf[size.x * size.y * size.z * ncomp * 16];
	struct type_info *ti = &type_info[fmt->type];

	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);

	for (int m = 0; m < miplevels; m++) {
		int w = u_minify(size.x, m);
		int h = u_minify(size.y, m);
		/* size in 3rd dim minifies for 3D but not 2D_ARRAY: */
		int slices = is_array(target) ? size.z : u_minify(size.z, m);
		void *ptr = texbuf;

		for (int s = 0; s < slices; s++) {
			ptr = ti->encode(ptr, ncomp, w, h, m, s);
		}

		switch (target) {
		case GL_TEXTURE_2D:
			glTexImage2D(target, m, fmt->ifmt, w, h, 0,
					fmt->ufmt, ti->type, texbuf);
			break;
		case GL_TEXTURE_3D:
		case GL_TEXTURE_2D_ARRAY:
			glTexImage3D(target, m, fmt->ifmt, w, h, slices, 0,
					fmt->ufmt, ti->type, texbuf);
			break;
		default:
			assert(!"bad target");
		}
	}
}

static int tex_handle;
static unsigned tex;

static void setup_gl(void)
{
	int prog, ret;

	prog = create_program(vertex_shader_source, get_fs());
	assert(prog >= 0);

	glBindAttribLocation(prog, IN_POSITION, "in_position");
	glBindAttribLocation(prog, IN_TEXCOORD, "in_texcoord");

	ret = link_program(prog);
	assert(ret == 0);

	glUseProgram(prog);

	glViewport(0, 0, gbm->width, gbm->height);

	glGenTextures(1, &tex);

	tex_handle = glGetUniformLocation(prog, "tex");
}

static void update_texture(void)
{
	/* calculate # of miplevels: */
	int max_dim;
	if (target == GL_TEXTURE_3D)
		max_dim = MAX3(size.x, size.y, size.z);
	else
		max_dim = MAX2(size.x, size.y);

	miplevels = (int) log2f(max_dim) + 1;

	glDeleteTextures(1, &tex);
	glGenTextures(1, &tex);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(target, tex);
	glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
	glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(target, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

	upload_texture();

	glUniform1i(tex_handle, 0); /* '0' refers to texture unit 0. */
}

static void draw_quad(float x, float y, float w, float h, float tw, float th, int m, int s)
{
	float in_position[4][4];
	float in_texcoord[4][4];

	/* convert from 0..1 to -1..1: */
	x = (x * 2.0) - 1.0;
	y = (y * 2.0) - 1.0;
	w *= 2.0;
	h *= 2.0;

	in_position[0][0] = x;
	in_position[0][1] = y;
	in_position[0][2] = 0.0;
	in_position[0][3] = 1.0;
	in_position[1][0] = x + w;
	in_position[1][1] = y;
	in_position[1][2] = 0.0;
	in_position[1][3] = 1.0;
	in_position[2][0] = x;
	in_position[2][1] = y + h;
	in_position[2][2] = 0.0;
	in_position[2][3] = 1.0;
	in_position[3][0] = x + w;
	in_position[3][1] = y + h;
	in_position[3][2] = 0.0;
	in_position[3][3] = 1.0;

	in_texcoord[0][0] = 0.0;
	in_texcoord[0][1] = 0.0;
	in_texcoord[0][2] = s;
	in_texcoord[0][3] = m;
	in_texcoord[1][0] = tw;
	in_texcoord[1][1] = 0.0;
	in_texcoord[1][2] = s;
	in_texcoord[1][3] = m;
	in_texcoord[2][0] = 0.0;
	in_texcoord[2][1] = th;
	in_texcoord[2][2] = s;
	in_texcoord[2][3] = m;
	in_texcoord[3][0] = tw;
	in_texcoord[3][1] = th;
	in_texcoord[3][2] = s;
	in_texcoord[3][3] = m;

	glVertexAttribPointer(IN_POSITION, 4, GL_FLOAT, GL_FALSE, 0, in_position);
	glEnableVertexAttribArray(IN_POSITION);

	glVertexAttribPointer(IN_TEXCOORD, 4, GL_FLOAT, GL_FALSE, 0, in_texcoord);
	glEnableVertexAttribArray(IN_TEXCOORD);

	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

static void extract_pix(uint8_t *rgba, int *slice, int *level, bool *complemented)
{
	*slice = (((float)rgba[0]) / 255.0) * 8.0;
	*level = (((float)rgba[1]) / 255.0) * 16.0;
	*complemented = (((float)rgba[2]) / 255.0) / 0.25 != 0.0f;
}

static bool probe_pix(int x, int y, int w, int h, int s, int m)
{
	uint32_t rgba[w*h], *ptr;
	bool err = false;

	glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, rgba);

	ptr = rgba;
	for (int i = 0; i < h; i++) {
		for (int j = 0; j < w; j++) {
			int slice, level;
			bool complemented;

			extract_pix((void *)(ptr++), &slice, &level, &complemented);
			if ((slice != s) || (level != m) || (complemented != ((i / zoom) & 1))) {
				printf("%ux%ux%u:%s: error at: S:L:C=%d:%d:%d, got %d:%d:%d at pix %d,%d (of %dx%d)\n",
					size.x, size.y, size.z, fmt->name,
					s, m, ((i / zoom) & 1), slice, level, complemented, j, i, w, h);
				err = true;
				if (!full)
					return err;
			}
		}
	}

	return err;
}

static bool check_quads(void)
{
	const int pad = 2;
	float y = pad;
	bool err = false;

	/* draw quads for each level/slice: */
	for (int m = 0; m < miplevels; m++) {
		float w = u_minify(size.x, m);
		float h = u_minify(size.y, m);
		/* size in 3rd dim minifies for 3D but not 2D_ARRAY: */
		int slices = is_array(target) ? size.z : u_minify(size.z, m);

		float x = pad;
		for (int s = 0; s < slices; s++) {
			int rx = x * zoom;
			int ry = y * zoom;

			if ((rx + w * zoom >= gbm->width) || (ry + h * zoom >= gbm->height))
				continue;

			err |= probe_pix(rx, ry, w*zoom, h*zoom, s, m);

			x += size.x + pad;
		}

		y += h + pad;
	}

	return err;
}

#ifdef HAVE_LIBPNG
static void write_png_file(char *filename, int width, int height, uint8_t *buffer)
{
	FILE *fp = fopen(filename, "wb");
	png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
	png_infop info = png_create_info_struct(png);

	png_init_io(png, fp);

	png_set_IHDR(
		png,
		info,
		width, height,
		8,
		PNG_COLOR_TYPE_RGBA,
		PNG_INTERLACE_NONE,
		PNG_COMPRESSION_TYPE_BASE,
		PNG_FILTER_TYPE_BASE
	);
	png_write_info(png, info);

	png_bytepp rows = (png_bytepp)png_malloc(png, height * sizeof(png_bytep));
	for (int i = 0; i < height; i++)
    	rows[i] = (png_bytep)(buffer + (height - i - 1) * width * 4);

	png_write_image(png, rows);

	png_write_end(png, NULL);

	fclose(fp);
	png_destroy_write_struct(&png, &info);
  	free(rows);
}
#endif

static bool needs_check = true;

static void draw_and_check_quads(unsigned frame)
{
	(void)frame;

	update_texture();

	if (needs_check)
		printf("Testing %dx%dx%d:%s\n", size.x, size.y, size.z, fmt->name);

	/* clear the color buffer */
	glClearColor(0.5, 0.5, 0.5, 1.0);
	glClear(GL_COLOR_BUFFER_BIT);

	float sw = gbm->width;
	float sh = gbm->height;

	sw /= zoom;
	sh /= zoom;

	const int pad = 2;
	float y = pad;

	/* draw quads for each level/slice: */
	for (int m = 0; m < miplevels; m++) {
		float w = u_minify(size.x, m);
		float h = u_minify(size.y, m);
		/* size in 3rd dim minifies for 3D but not 2D_ARRAY: */
		int slices = is_array(target) ? size.z : u_minify(size.z, m);

		float x = pad;
		for (int s = 0; s < slices; s++) {
			draw_quad(x/sw, y/sh, w/sw, h/sh, w, h, m, s);
			x += size.x + pad;
		}

		y += h + pad;
	}

	if (needs_check) {
		glFlush();
		bool err = check_quads();
		if (err)
			error_frames++;
		needs_check = false;

#ifdef HAVE_LIBPNG
		if (png) {
			uint8_t rgba[gbm->width*gbm->height*4];
			glReadPixels(0, 0, gbm->width, gbm->height, GL_RGBA, GL_UNSIGNED_BYTE, rgba);
			static char buf[64];
			sprintf(buf, "kmscube-texturator-%dx%dx%d:%s.png",
					size.x, size.y, size.z, fmt->name);
			write_png_file(buf, gbm->width, gbm->height, rgba);
		}
#endif
	}

	/* if we've hit max # of error frames, stop growing: */
	if (error_frames >= max_error_frames)
		goto bail;

	if ((size.x < maxsz.x) || (size.y < maxsz.y) || (size.z < maxsz.z)) {
		/* Increase width first, then height, then depth: */
		size.x++;
		if (size.x > maxsz.x) {
			size.x = minsz.x;
			size.y++;
		}
		if (size.y > maxsz.y) {
			size.x = minsz.x;
			size.y = minsz.y;
			size.z++;
		}
		assert(size.z <= maxsz.z);
		needs_check = true;

		return;
	}

bail:
	if (stop) {
		printf("Exiting with %d errors\n", error_frames);
		exit((error_frames > 0) ? -1 : 0);
	}
}

static void print_summary(void)
{
	printf("testing %s %s at %ux%ux%u-%ux%ux%u with %dx zoom\n", fmt->name,
		get_sampler(), minsz.x, minsz.y, minsz.z,
		maxsz.x, maxsz.y, maxsz.z, zoom);
	printf("VS:\n%s\n", vertex_shader_source);
	printf("FS:\n%s\n", get_fs());
}

static const char *shortopts = "D:e:fsv:z";

static const struct option longopts[] = {
	{"device", required_argument, 0, 'D'},
	{"errors", required_argument, 0, 'e'},
	{"full",   no_argument,       0, 'f'},
	{"stop",   no_argument,       0, 's'},
	{"vmode",  required_argument, 0, 'v'},
	{"zoom",   no_argument,       0, 'z'},
#ifdef HAVE_LIBPNG
	{"png",    no_argument,       0, 'p'},
#endif
	{0, 0, 0, 0}
};

static void usage(const char *name)
{
	printf("Usage: %1$s [-Dvz] <target> <format> <minsize> [<maxsize>]\n"
		"\n"
		"options:\n"
		"    -D, --device=DEVICE  use the given device\n"
		"    -e, --errors=N       stop after N frames with errors (default 5)\n"
		"    -f, --full           check all pixels (do not stop after first faulty pixel)\n"
		"    -s, --stop           exit after testing all sizes\n"
		"    -v, --vmode=VMODE    specify the video mode in the format\n"
		"                         <mode>[-<vrefresh>]\n"
		"    -z, --zoom           increase zoom (can be specified multiple times)\n"
#ifdef HAVE_LIBPNG
		"    -p, --png            capture the screen to a png image\n"
#endif
		"\n"
		"where:\n"
		"    <target>  is one of 2D/2DArray/3D\n"
		"    <format>  is a GL sized internal-format without GL_ prefix\n"
		"    <size>    is XxY (2D) or XxYxZ (2DArray/3D)\n"
		"\n"
		"example:\n"
		"    %1$s -z 3D RG16UI 37x65x4\n"
		, name);
	exit(-1);
}

static void parse_dims(const char *argv0, const char *sizestr, struct size *size)
{
	if (target == GL_TEXTURE_2D) {
		if (sscanf(sizestr, "%ux%ux", &size->x, &size->y) != 2) {
			printf("invalid size: %s\n", sizestr);
			usage(argv0);
		}
		size->z = 1;
	} else {
		if (sscanf(sizestr, "%ux%ux%ux", &size->x, &size->y, &size->z) != 3) {
			printf("invalid size: %s\n", sizestr);
			usage(argv0);
		}
	}
}

int main(int argc, char *argv[])
{
	const char *device = "/dev/dri/card0";
	char mode_str[DRM_DISPLAY_MODE_LEN] = "";
	char *p;
	int ret, opt;
	unsigned int len;
	unsigned int vrefresh = 0;

	while ((opt = getopt_long_only(argc, argv, shortopts, longopts, NULL)) != -1) {
		switch (opt) {
		case 'D':
			device = optarg;
			break;
		case 'e':
			max_error_frames = atoi(optarg);
			break;
		case 'f':
			full = true;
			break;
		case 's':
			stop = true;
			break;
		case 'v':
			p = strchr(optarg, '-');
			if (p == NULL) {
				len = strlen(optarg);
			} else {
				vrefresh = strtoul(p + 1, NULL, 0);
				len = p - optarg;
			}
			if (len > sizeof(mode_str) - 1)
				len = sizeof(mode_str) - 1;
			strncpy(mode_str, optarg, len);
			mode_str[len] = '\0';
			break;
		case 'z':
			zoom++;
			break;
#ifdef HAVE_LIBPNG
		case 'p':
			png = true;
			break;
#endif
		default:
			usage(argv[0]);
		}
	}

	if ((optind + 2) >= argc) {
		usage(argv[0]);
	}

	/* parse remaining args: */
	const char *targetstr = argv[optind + 0];
	const char *fmtstr  = argv[optind + 1];
	const char *minstr  = argv[optind + 2];
	const char *maxstr  = ((optind + 3) < argc) ? argv[optind + 3] : NULL;

	if (!strcmp(targetstr, "2D")) {
		target = GL_TEXTURE_2D;
	} else if (!strcmp(targetstr, "2DArray")) {
		target = GL_TEXTURE_2D_ARRAY;
	} else if (!strcmp(targetstr, "3D")) {
		target = GL_TEXTURE_3D;
	} else {
		printf("invalid target: %s\n", targetstr);
		usage(argv[0]);
	}

	for (unsigned i = 0; i < ARRAY_SIZE(fmts); i++) {
		if (!strcmp(fmtstr, fmts[i].name)) {
			fmt = &fmts[i];
			break;
		}
	}

	if (!fmt) {
		printf("invalid format: %s\n", fmtstr);
		usage(argv[0]);
	}

	parse_dims(argv[0], minstr, &minsz);

	if (maxstr) {
		parse_dims(argv[0], maxstr, &maxsz);
	} else {
		maxsz = minsz;
	}

	size = minsz;

	print_summary();

	/* no real need for atomic here: */
	drm = init_drm_legacy(device, mode_str, vrefresh, ~0);
	if (!drm) {
		printf("failed to initialize DRM\n");
		return -1;
	}

	gbm = init_gbm(drm->fd, drm->mode->hdisplay, drm->mode->vdisplay,
			DRM_FORMAT_XRGB8888, DRM_FORMAT_MOD_LINEAR, false);
	if (!gbm) {
		printf("failed to initialize GBM\n");
		return -1;
	}

	ret = init_egl(&_egl, gbm, 0);
	if (ret) {
		printf("failed to initialize EGL\n");
		return -1;
	}

	_egl.draw = draw_and_check_quads;

	setup_gl();

	return drm->run(gbm, egl);
}
