#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <linux/fb.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <linux/vt.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <math.h>

#include "hardware.h"
#include "user_io.h"
#include "spi.h"
#include "cfg.h"
#include "file_io.h"
#include "mat4x4.h"
#include "menu.h"
#include "video.h"
#include "input.h"
#include "shmem.h"
#include "smbus.h"
#include "str_util.h"
#include "profiling.h"
#include "offload.h"

#include "support.h"
#include "support/arcade/mra_loader.h"
#include "lib/imlib2/Imlib2.h"
#include "lib/md5/md5.h"

#define FB_SIZE  (1920*1080)
#define FB_ADDR  (0x20000000 + (32*1024*1024)) // 512mb + 32mb(Core's fb)

/*
--  [2:0] : 011=8bpp(palette) 100=16bpp 101=24bpp 110=32bpp
--  [3]   : 0=16bits 565 1=16bits 1555
--  [4]   : 0=RGB  1=BGR (for 16/24/32 modes)
--  [5]   : TBD
*/

#define FB_FMT_565  0b00100
#define FB_FMT_1555 0b01100
#define FB_FMT_888  0b00101
#define FB_FMT_8888 0b00110
#define FB_FMT_PAL8 0b00011
#define FB_FMT_RxB  0b10000
#define FB_EN       0x8000

#define FB_DV_LBRD  3
#define FB_DV_RBRD  6
#define FB_DV_UBRD  2
#define FB_DV_BBRD  2

#define VRR_NONE     0x00
#define VRR_FREESYNC 0x01
#define VRR_VESA     0x02

static int     use_vrr = 0;
static int     use_freesync_spd = 0;
static uint8_t vrr_min_fr = 0;
static uint8_t vrr_max_fr = 0;

static volatile uint32_t *fb_base = 0;
static int fb_enabled = 0;
static int fb_width = 0;
static int fb_height = 0;
static int fb_num = 0;
static int brd_x = 0;
static int brd_y = 0;

static int menu_bg = 0;
static int menu_bgn = 0;

VideoInfo current_video_info;

static int support_FHD = 0;

yc_mode yc_modes[20];

struct vrr_cap_t
{
	uint8_t active;
	uint8_t available;
	uint8_t min_fr;
	uint8_t max_fr;
	char description[128];
};

static vrr_cap_t vrr_modes[3] = {
	{0, 0, 0, 0, "None"},
	{0, 0, 0, 0, "AMD Freesync"},
	{0, 0, 0, 0, "Vesa Forum VRR"},
};

static uint8_t last_vrr_mode = 0xFF;
static float last_vrr_rate = 0.0f;
static uint32_t last_vrr_vfp = 0;
static uint8_t edid[256] = {};

struct vmode_t
{
	uint32_t vpar[8];
	double Fpix;
	uint8_t vic_mode;
	uint8_t pr;
};

vmode_t vmodes[] =
{
	{ { 1280, 110,  40, 220,  720,  5,  5, 20 },  74.25,  4, 0 }, //0  1280x720@60
	{ { 1024,  24, 136, 160,  768,  3,  6, 29 },  65,     0, 0 }, //1  1024x768@60
	{ {  720,  16,  62,  60,  480,  9,  6, 30 },  27,     3, 0 }, //2  720x480@60
	{ {  720,  12,  64,  68,  576,  5,  5, 39 },  27,    18, 0 }, //3  720x576@50
	{ { 1280,  48, 112, 248, 1024,  1,  3, 38 }, 108,     0, 0 }, //4  1280x1024@60
	{ {  800,  40, 128,  88,  600,  1,  4, 23 },  40,     0, 0 }, //5  800x600@60
	{ {  640,  16,  96,  48,  480, 10,  2, 33 },  25.175, 1, 0 }, //6  640x480@60
	{ { 1280, 440,  40, 220,  720,  5,  5, 20 },  74.25, 19, 0 }, //7  1280x720@50
	{ { 1920,  88,  44, 148, 1080,  4,  5, 36 }, 148.5,  16, 0 }, //8  1920x1080@60
	{ { 1920, 528,  44, 148, 1080,  4,  5, 36 }, 148.5,  31, 0 }, //9  1920x1080@50
	{ { 1366,  70, 143, 213,  768,  3,  3, 24 },  85.5,   0, 0 }, //10 1366x768@60
	{ { 1024,  40, 104, 144,  600,  1,  3, 18 },  48.96,  0, 0 }, //11 1024x600@60
	{ { 1920,  48,  32,  80, 1440,  2,  4, 38 }, 185.203, 0, 0 }, //12 1920x1440@60
	{ { 2048,  48,  32,  80, 1536,  2,  4, 38 }, 209.318, 0, 0 }, //13 2048x1536@60
	{ { 1280,  24,  16,  40, 1440,  3,  5, 33 }, 120.75,  0, 1 }, //14 2560x1440@60 (pr)
};
#define VMODES_NUM (sizeof(vmodes) / sizeof(vmodes[0]))

vmode_t tvmodes[] =
{
	{{ 640, 30, 60, 70, 240,  4, 4, 14 }, 12.587, 0, 0 }, //NTSC 15K
	{{ 640, 16, 96, 48, 480,  8, 4, 33 }, 25.175, 0, 0 }, //NTSC 31K
	{{ 640, 30, 60, 70, 288,  6, 4, 14 }, 12.587, 0, 0 }, //PAL 15K
	{{ 640, 16, 96, 48, 576,  2, 4, 42 }, 25.175, 0, 0 }, //PAL 31K
};

// named aliases for vmode_custom_t items
struct vmode_custom_param_t
{
	uint32_t mode;

	// [1]
	uint32_t hact;
	uint32_t hfp;
	uint32_t hs;
	uint32_t hbp;

	// [5]
	uint32_t vact;
	uint32_t vfp;
	uint32_t vs;
	uint32_t vbp;

	// [9]
	uint32_t pll[12];

	// [21]
	uint32_t hpol;
	uint32_t vpol;
	uint32_t vic;
	uint32_t rb;
	uint32_t pr;

	// [26]
	uint32_t unused[6];
};

struct vmode_custom_t
{
	union // anonymous
	{
		vmode_custom_param_t param;
		uint32_t item[32];
	};

	double Fpix;
};

static_assert(sizeof(vmode_custom_param_t) == sizeof(vmode_custom_t::item));

// Static fwd decl
static void video_fb_config();
static void video_calculate_cvt(int horiz_pixels, int vert_pixels, float refresh_rate, int reduced_blanking, vmode_custom_t *vmode);

static vmode_custom_t v_cur = {}, v_def = {}, v_pal = {}, v_ntsc = {};
static int vmode_def = 0, vmode_pal = 0, vmode_ntsc = 0;

static bool supports_pr()
{
	static uint16_t video_version = 0xffff;
	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 1;
	return video_version != 0;
}

static bool supports_vrr()
{
	static uint16_t video_version = 0xffff;
	if (video_version == 0xffff) video_version = spi_uio_cmd(UIO_SET_VIDEO) & 2;
	return video_version != 0;
}

static uint32_t getPLLdiv(uint32_t div)
{
	if (div & 1) return 0x20000 | (((div / 2) + 1) << 8) | (div / 2);
	return ((div / 2) << 8) | (div / 2);
}

static int findPLLpar(double Fout, uint32_t *pc, uint32_t *pm, double *pko)
{
	uint32_t c = 1;
	while ((Fout*c) < 400) c++;

	while (1)
	{
		double fvco = Fout*c;
		uint32_t m = (uint32_t)(fvco / 50);
		double ko = ((fvco / 50) - m);

		fvco = ko + m;
		fvco *= 50.f;

		if (ko && (ko <= 0.05f || ko >= 0.95f))
		{
			printf("Fvco=%f, C=%d, M=%d, K=%f ", fvco, c, m, ko);
			if (fvco > 1500.f)
			{
				printf("-> No exact parameters found\n");
				return 0;
			}
			printf("-> K is outside allowed range\n");
			c++;
		}
		else
		{
			*pc = c;
			*pm = m;
			*pko = ko;
			return 1;
		}
	}

	//will never reach here
	return 0;
}

static void setPLL(double Fout, vmode_custom_t *v)
{
	PROFILE_FUNCTION();

	double Fpix;
	double fvco, ko;
	uint32_t m, c;

	printf("Calculate PLL for %.4f MHz:\n", Fout);

	if (!findPLLpar(Fout, &c, &m, &ko))
	{
		c = 1;
		while ((Fout*c) < 400) c++;

		fvco = Fout*c;
		m = (uint32_t)(fvco / 50);
		ko = ((fvco / 50) - m);

		//Make sure K is in allowed range.
		if (ko <= 0.05f)
		{
			ko = 0;
		}
		else if (ko >= 0.95f)
		{
			m++;
			ko = 0;
		}
	}

	uint32_t k = ko ? (uint32_t)(ko * 4294967296) : 1;

	fvco = ko + m;
	fvco *= 50.f;
	Fpix = fvco / c;

	printf("Fvco=%f, C=%d, M=%d, K=%f(%u) -> Fpix=%f\n", fvco, c, m, ko, k, Fpix);

	v->item[9]  = 4;
	v->item[10] = getPLLdiv(m);
	v->item[11] = 3;
	v->item[12] = 0x10000;
	v->item[13] = 5;
	v->item[14] = getPLLdiv(c);
	v->item[15] = 9;
	v->item[16] = 2;
	v->item[17] = 8;
	v->item[18] = 7;
	v->item[19] = 7;
	v->item[20] = k;

	v->Fpix = Fpix;
}

struct ScalerFilter
{
	char mode;
	char filename[1023];
};

static ScalerFilter scaler_flt[4];

struct FilterPhase
{
	short t[4];
};

static constexpr int N_PHASES = 256;

struct VideoFilterDigest
{
	VideoFilterDigest() { memset(md5, 0, sizeof(md5)); }
	bool operator!=(const VideoFilterDigest& other) { return memcmp(md5, other.md5, sizeof(md5)) != 0; }
	bool operator==(const VideoFilterDigest& other) { return memcmp(md5, other.md5, sizeof(md5)) == 0; }

	unsigned char md5[16];
};

struct VideoFilter
{
	bool is_adaptive;
	FilterPhase phases[N_PHASES];
	FilterPhase adaptive_phases[N_PHASES];
	VideoFilterDigest digest;
};

static VideoFilter scaler_flt_data[4];

static bool scale_phases(FilterPhase out_phases[N_PHASES], FilterPhase *in_phases, int in_count)
{
	if (!in_count)
	{
		return false;
	}

	int dup = N_PHASES / in_count;

	if ((in_count * dup) != N_PHASES)
	{
		return false;
	}

	for (int i = 0; i < in_count; i++)
	{
		for (int j = 0; j < dup; j++)
		{
			out_phases[(i * dup) + j] = in_phases[i];
		}
	}

	return true;
}

static bool read_video_filter(int type, VideoFilter *out)
{
	PROFILE_FUNCTION();

	fileTextReader reader = {};
	FilterPhase phases[512];
	int count = 0;
	bool is_adaptive = false;
	int scale = 2;

	memset(out, 0, sizeof(VideoFilter));

	static char filename[1024];
	snprintf(filename, sizeof(filename), COEFF_DIR"/%s", scaler_flt[type].filename);

	if (FileOpenTextReader(&reader, filename))
	{
		const char *line;
		while ((line = FileReadLine(&reader)))
		{
			if (count == 0 && !strcasecmp(line, "adaptive"))
			{
				is_adaptive = true;
				continue;
			}

			if (count == 0 && !strcasecmp(line, "10bit"))
			{
				scale = 1;
				continue;
			}

			int phase[4];
			int n = sscanf(line, "%d,%d,%d,%d", &phase[0], &phase[1], &phase[2], &phase[3]);
			if (n == 4)
			{
				if (count >= (is_adaptive ? N_PHASES * 2 : N_PHASES)) return false; //too many
				phases[count].t[0] = phase[0] * scale;
				phases[count].t[1] = phase[1] * scale;
				phases[count].t[2] = phase[2] * scale;
				phases[count].t[3] = phase[3] * scale;
				count++;
			}
		}
	}

	printf( "Filter \'%s\', phases: %d adaptive: %s\n",
			scaler_flt[type].filename,
			is_adaptive ? count / 2 : count,
			is_adaptive ? "true" : "false" );

	bool valid = false;
	if (is_adaptive)
	{
		out->is_adaptive = true;
		valid = scale_phases(out->phases, phases, count / 2);
		valid = valid && scale_phases(out->adaptive_phases, phases + (count / 2), count / 2);
	}
	else if (count == 32 && !is_adaptive) // legacy
	{
		out->is_adaptive = false;
		valid = scale_phases(out->phases, phases, 16);
	}
	else if (!is_adaptive)
	{
		out->is_adaptive = false;
		valid = scale_phases(out->phases, phases, count);
	}

	if (!valid)
	{
		// Make a default NN filter in case of error
		out->is_adaptive = false;
		FilterPhase nn_phases[2] =
		{
			{ .t = { 0, 256, 0, 0 } },
			{ .t = { 0, 0, 256, 0 } }
		};
		scale_phases(out->phases, nn_phases, 2);
	}

	MD5Context ctx;
	MD5Init(&ctx);
	MD5Update(&ctx, (unsigned char *)&out->is_adaptive, sizeof(VideoFilter::is_adaptive));
	MD5Update(&ctx, (unsigned char *)out->phases, sizeof(VideoFilter::phases));
	MD5Update(&ctx, (unsigned char *)out->adaptive_phases, sizeof(VideoFilter::adaptive_phases));
	MD5Final(out->digest.md5, &ctx);

	return valid;
}

static void send_phases_legacy(int addr, const FilterPhase phases[N_PHASES])
{
	PROFILE_FUNCTION();

	for (int idx = 0; idx < N_PHASES; idx += 16)
	{
		const FilterPhase *p = &phases[idx];
		spi_w(((p->t[0] >> 1) & 0x1FF) | ((addr + 0) << 9));
		spi_w(((p->t[1] >> 1) & 0x1FF) | ((addr + 1) << 9));
		spi_w(((p->t[2] >> 1) & 0x1FF) | ((addr + 2) << 9));
		spi_w(((p->t[3] >> 1) & 0x1FF) | ((addr + 3) << 9));
		addr += 4;
	}
}

static void send_phases(int addr, const FilterPhase phases[N_PHASES], bool full_precision)
{
	PROFILE_FUNCTION();

	const int skip = full_precision ? 1 : 4;
	const int shift = full_precision ? 0 : 1;

	addr *= full_precision ? (N_PHASES * 4) : (64 * 4);

	for (int idx = 0; idx < N_PHASES; idx += skip)
	{
		const FilterPhase *p = &phases[idx];
		spi_w(addr + 0); spi_w((p->t[0] >> shift) & 0x3FF);
		spi_w(addr + 1); spi_w((p->t[1] >> shift) & 0x3FF);
		spi_w(addr + 2); spi_w((p->t[2] >> shift) & 0x3FF);
		spi_w(addr + 3); spi_w((p->t[3] >> shift) & 0x3FF);
		addr += 4;
	}
}

static VideoFilterDigest horiz_filter_digest, vert_filter_digest;

static void send_video_filters(const VideoFilter *horiz, const VideoFilter *vert, int ver)
{
	PROFILE_FUNCTION();

	spi_uio_cmd_cont(UIO_SET_FLTCOEF);

	const bool full_precision = (ver & 0x4) != 0;

	const bool send_horiz = horiz_filter_digest != horiz->digest;
	const bool send_vert = vert_filter_digest != vert->digest;

	switch( ver & 0x3 )
	{
		case 1:
			if (send_horiz) send_phases_legacy(0, horiz->phases);
			if (send_vert) send_phases_legacy(64, vert->phases);
			break;
		case 2:
			if (send_horiz) send_phases(0, horiz->phases, full_precision);
			if (send_vert) send_phases(1, vert->phases, full_precision);
			break;
		case 3:
			if (send_horiz) send_phases(0, horiz->phases, full_precision);
			if (send_vert) send_phases(1, vert->phases, full_precision);

			if (horiz->is_adaptive && send_horiz)
			{
				send_phases(2, horiz->adaptive_phases, full_precision);
			}
			else if (vert->is_adaptive && send_vert)
			{
				send_phases(3, vert->adaptive_phases, full_precision);
			}
			break;
		default:
			break;
	}

	horiz_filter_digest = horiz->digest;
	vert_filter_digest = vert->digest;

	DisableIO();
}

static void set_vfilter(int force)
{
	PROFILE_FUNCTION();


	static int last_flags = 0;

	int flt_flags = spi_uio_cmd_cont(UIO_SET_FLTNUM);
	if (!flt_flags || (!force && last_flags == flt_flags))
	{
		DisableIO();
		return;
	}

	last_flags = flt_flags;
	printf("video_set_filter: flt_flags=%d\n", flt_flags);

	spi8(scaler_flt[0].mode);
	DisableIO();

	int vert_flt;
	if (current_video_info.interlaced) vert_flt = scaler_flt[VFILTER_ILACE].mode ? VFILTER_ILACE : VFILTER_HORZ;
	else if ((flt_flags & 0x30) && scaler_flt[VFILTER_SCAN].mode) vert_flt = VFILTER_SCAN;
	else if (scaler_flt[VFILTER_VERT].mode) vert_flt = VFILTER_VERT;
	else vert_flt = VFILTER_HORZ;

	send_video_filters(&scaler_flt_data[VFILTER_HORZ], &scaler_flt_data[vert_flt], flt_flags & 0xF);
}

static void setScaler()
{
	PROFILE_FUNCTION();

	uint32_t arc[4] = {};
	for (int i = 0; i < 2; i++)
	{
		if (cfg.custom_aspect_ratio[i][0])
		{
			if (sscanf(cfg.custom_aspect_ratio[i], "%u:%u", &arc[i * 2], &arc[(i * 2) + 1]) != 2 || arc[i * 2] < 1 || arc[i * 2] > 4095 || arc[(i * 2) + 1] < 1 || arc[(i * 2) + 1] > 4095)
			{
				arc[(i * 2) + 0] = 0;
				arc[(i * 2) + 1] = 0;
			}
		}
	}

	spi_uio_cmd_cont(UIO_SET_AR_CUST);
	for (int i = 0; i < 4; i++) spi_w(arc[i]);
	DisableIO();
	set_vfilter(1);
}

int video_get_scaler_flt(int type)
{
	return scaler_flt[type].mode;
}

char* video_get_scaler_coeff(int type, int only_name)
{
	char *path = scaler_flt[type].filename;
	if (only_name)
	{
		char *p = strrchr(path, '/');
		if (p) return p + 1;
	}
	return path;
}

static char scaler_cfg_path[128] = { 0 };

static void video_save_scaler_cfg()
{
	FileSaveConfig(scaler_cfg_path, &scaler_flt, sizeof(scaler_flt));
}

static void video_apply_scaler_flt(int type, int n)
{
	scaler_flt[type].mode = (char)n;
	spi_uio_cmd8(UIO_SET_FLTNUM, scaler_flt[0].mode);
	set_vfilter(1);
}

void video_set_scaler_flt(int type, int n)
{
	video_apply_scaler_flt(type, n);
	video_save_scaler_cfg();
}

void video_apply_scaler_coeff(int type, const char *name)
{
	strcpy(scaler_flt[type].filename, name);
	read_video_filter(type, &scaler_flt_data[type]);
	setScaler();
	user_io_send_buttons(1);
}

void video_set_scaler_coeff(int type, const char *name)
{
	video_apply_scaler_coeff(type, name);
	video_save_scaler_cfg();
}

static void loadScalerCfg()
{
	PROFILE_FUNCTION();

	if (FileLoadConfig(scaler_cfg_path, &scaler_flt, sizeof(scaler_flt)))
	{
		if (scaler_flt[0].mode > 1)
		{
			memset(scaler_flt, 0, sizeof(scaler_flt));
		}
	}
	else
	{
		if (cfg.vfilter_default[0])
		{
			strcpy(scaler_flt[VFILTER_HORZ].filename, cfg.vfilter_default);
			scaler_flt[VFILTER_HORZ].mode = 1;
		}

		if (cfg.vfilter_vertical_default[0])
		{
			strcpy(scaler_flt[VFILTER_VERT].filename, cfg.vfilter_vertical_default);
			scaler_flt[VFILTER_VERT].mode = 1;
		}

		if (cfg.vfilter_scanlines_default[0])
		{
			strcpy(scaler_flt[VFILTER_SCAN].filename, cfg.vfilter_scanlines_default);
			scaler_flt[VFILTER_SCAN].mode = 1;
		}

		if (cfg.vfilter_interlace_default[0])
		{
			strcpy(scaler_flt[VFILTER_ILACE].filename, cfg.vfilter_interlace_default);
			scaler_flt[VFILTER_ILACE].mode = 1;
		}
	}

	if (!read_video_filter(VFILTER_HORZ, &scaler_flt_data[VFILTER_HORZ])) memset(&scaler_flt[VFILTER_HORZ], 0, sizeof(scaler_flt[VFILTER_HORZ]));
	if (!read_video_filter(VFILTER_VERT, &scaler_flt_data[VFILTER_VERT])) memset(&scaler_flt[VFILTER_VERT], 0, sizeof(scaler_flt[VFILTER_VERT]));
	if (!read_video_filter(VFILTER_SCAN, &scaler_flt_data[VFILTER_SCAN])) memset(&scaler_flt[VFILTER_SCAN], 0, sizeof(scaler_flt[VFILTER_SCAN]));
	if (!read_video_filter(VFILTER_ILACE, &scaler_flt_data[VFILTER_ILACE])) memset(&scaler_flt[VFILTER_ILACE], 0, sizeof(scaler_flt[VFILTER_ILACE]));
}

static char active_gamma_cfg[1024] = { 0 };
static char gamma_cfg[1024] = { 0 };
static char has_gamma = 0; // set in video_init

static void setGamma()
{
	PROFILE_FUNCTION();

	if (!memcmp(active_gamma_cfg, gamma_cfg, sizeof(gamma_cfg))) return;

	fileTextReader reader = {};
	static char filename[1024];

	if (!has_gamma) return;

	snprintf(filename, sizeof(filename), GAMMA_DIR"/%s", gamma_cfg + 1);

	if (FileOpenTextReader(&reader, filename))
	{
		spi_uio_cmd_cont(UIO_SET_GAMCURV);

		const char *line;
		int index = 0;
		while ((line = FileReadLine(&reader)))
		{
			int c0, c1, c2;
			int n = sscanf(line, "%d,%d,%d", &c0, &c1, &c2);
			if (n == 1)
			{
				c1 = c0;
				c2 = c0;
				n = 3;
			}

			if (n == 3)
			{
				spi_w((index << 8) | (c0 & 0xFF));
				spi_w((index << 8) | (c1 & 0xFF));
				spi_w((index << 8) | (c2 & 0xFF));

				index++;
				if (index >= 256) break;
			}
		}
		DisableIO();
		spi_uio_cmd8(UIO_SET_GAMMA, gamma_cfg[0]);
	}
	memcpy(active_gamma_cfg, gamma_cfg, sizeof(gamma_cfg));
}

int video_get_gamma_en()
{
	return has_gamma ? gamma_cfg[0] : -1;
}

char* video_get_gamma_curve(int only_name)
{
	char *path = gamma_cfg + 1;
	if (only_name)
	{
		char *p = strrchr(path, '/');
		if (p) return p + 1;
	}
	return path;
}

static char gamma_cfg_path[1024] = { 0 };
static void video_save_gamma_cfg()
{
	FileSaveConfig(gamma_cfg_path, &gamma_cfg, sizeof(gamma_cfg));
}

static void video_apply_gamma_en(int n)
{
	gamma_cfg[0] = (char)n;
	setGamma();
}

void video_set_gamma_en(int n)
{
	video_apply_gamma_en(n);
	video_save_gamma_cfg();
}

static void video_apply_gamma_curve(const char *name)
{
	strcpy(gamma_cfg + 1, name);
	setGamma();
	user_io_send_buttons(1);
}

void video_set_gamma_curve(const char *name)
{
	video_apply_gamma_curve(name);
	video_save_gamma_cfg();
}


static void loadGammaCfg()
{
	PROFILE_FUNCTION();
	if (FileLoadConfig(gamma_cfg_path, &gamma_cfg, sizeof(gamma_cfg) - 1))
	{
		if (gamma_cfg[0] > 1)
		{
			memset(gamma_cfg, 0, sizeof(gamma_cfg));
		}
	}
}

static char shadow_mask_cfg[1024] = { 0 };
static bool has_shadow_mask = false;

#define SM_FLAG_2X      ( 1 << 1 )
#define SM_FLAG_ROTATED ( 1 << 2 )
#define SM_FLAG_ENABLED ( 1 << 3 )

#define SM_FLAG(v) ( ( 0x0 << 13 ) | (v) )
#define SM_VMAX(v) ( ( 0x1 << 13 ) | (v) )
#define SM_HMAX(v) ( ( 0x2 << 13 ) | (v) )
#define SM_LUT(v)  ( ( 0x3 << 13 ) | (v) )

enum
{
	SM_MODE_NONE = 0,
	SM_MODE_1X,
	SM_MODE_2X,
	SM_MODE_1X_ROTATED,
	SM_MODE_2X_ROTATED,
	SM_MODE_COUNT
};

static void setShadowMask()
{
	PROFILE_FUNCTION();

	static char filename[1024];
	has_shadow_mask = 0;

	if (!spi_uio_cmd_cont(UIO_SHADOWMASK))
	{
		DisableIO();
		return;
	}

	has_shadow_mask = 1;
	switch (video_get_shadow_mask_mode())
	{
		default: spi_w(SM_FLAG(0)); break;
		case SM_MODE_1X: spi_w(SM_FLAG(SM_FLAG_ENABLED)); break;
		case SM_MODE_2X: spi_w(SM_FLAG(SM_FLAG_ENABLED | SM_FLAG_2X)); break;
		case SM_MODE_1X_ROTATED: spi_w(SM_FLAG(SM_FLAG_ENABLED | SM_FLAG_ROTATED)); break;
		case SM_MODE_2X_ROTATED: spi_w(SM_FLAG(SM_FLAG_ENABLED | SM_FLAG_ROTATED | SM_FLAG_2X)); break;
	}

	int loaded = 0;
	snprintf(filename, sizeof(filename), SMASK_DIR"/%s", shadow_mask_cfg + 1);

	fileTextReader reader;
	if (FileOpenTextReader(&reader, filename))
	{
		char *start_pos = reader.pos;
		const char *line;
		uint32_t res = 0;
		while ((line = FileReadLine(&reader)))
		{
			if (!strncasecmp(line, "resolution=", 11))
			{
				if (sscanf(line + 11, "%u", &res))
				{
					if (v_cur.item[5] >= res)
					{
						start_pos = reader.pos;
					}
				}
			}
		}

		int w = -1, h = -1;
		int y = 0;
		int v2 = 0;

		reader.pos = start_pos;
		while ((line = FileReadLine(&reader)))
		{
			if (w == -1)
			{
				if (!strcasecmp(line, "v2"))
				{
					v2 = 1;
					continue;
				}

				if (!strncasecmp(line, "resolution=", 11))
				{
					continue;
				}

				int n = sscanf(line, "%d,%d", &w, &h);
				if ((n != 2) || (w <= 0) || (h <= 0) || (w > 16) || (h > 16))
				{
					break;
				}
			}
			else
			{
				unsigned int p[16];
				int n = sscanf(line, "%X,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x", p + 0, p + 1, p + 2, p + 3, p + 4, p + 5, p + 6, p + 7, p + 8, p + 9, p + 10, p + 11, p + 12, p + 13, p + 14, p + 15);
				if (n != w)
				{
					break;
				}

				for (int x = 0; x < 16; x++) spi_w(SM_LUT(v2 ? (p[x] & 0x7FF) : (((p[x] & 7) << 8) | 0x2A)));
				y += 1;

				if (y == h)
				{
					loaded = 1;
					break;
				}
			}
		}

		if (y == h)
		{
			spi_w(SM_HMAX(w - 1));
			spi_w(SM_VMAX(h - 1));
		}
	}

	if (!loaded) spi_w(SM_FLAG(0));
	DisableIO();
}

int video_get_shadow_mask_mode()
{
	return has_shadow_mask ? shadow_mask_cfg[0] : -1;
}

char* video_get_shadow_mask(int only_name)
{
	char *path = shadow_mask_cfg + 1;
	if (only_name)
	{
		char *p = strrchr(path, '/');
		if (p) return p + 1;
	}
	return path;
}

static char shadow_mask_cfg_path[1024] = { 0 };

static void video_save_shadow_mask_cfg()
{
	FileSaveConfig(shadow_mask_cfg_path, &shadow_mask_cfg, sizeof(shadow_mask_cfg));
}

static void video_apply_shadow_mask_mode(int n)
{
	if( n >= SM_MODE_COUNT )
	{
		n = 0;
	}
	else if (n < 0)
	{
		n = SM_MODE_COUNT - 1;
	}

	shadow_mask_cfg[0] = (char)n;
	setShadowMask();
}

void video_set_shadow_mask_mode(int n)
{
	video_apply_shadow_mask_mode(n);
	video_save_shadow_mask_cfg();
}

static void video_apply_shadow_mask(const char *name)
{
	strcpy(shadow_mask_cfg + 1, name);
	setShadowMask();
	user_io_send_buttons(1);
}

void video_set_shadow_mask(const char *name)
{
	video_apply_shadow_mask(name);
	video_save_shadow_mask_cfg();
}

static void loadShadowMaskCfg()
{
	PROFILE_FUNCTION();

	if (!FileLoadConfig(shadow_mask_cfg_path, &shadow_mask_cfg, sizeof(shadow_mask_cfg) - 1))
	{
		if (cfg.shmask_default[0])
		{
			strcpy(shadow_mask_cfg + 1, cfg.shmask_default);
			shadow_mask_cfg[0] = cfg.shmask_mode_default;
		}
	}

	if( shadow_mask_cfg[0] >= SM_MODE_COUNT )
	{
		shadow_mask_cfg[0] = 0;
	}
}


#define IS_NEWLINE(c) (((c) == '\r') || ((c) == '\n'))
#define IS_WHITESPACE(c) (IS_NEWLINE(c) || ((c) == ' ') || ((c) == '\t'))

static char* get_preset_arg(const char *str)
{
	static char par[1024];
	snprintf(par, sizeof(par), "%s", str);
	char *pos = par;

	while (*pos && !IS_NEWLINE(*pos)) pos++;
	*pos-- = 0;

	while (pos >= par)
	{
		if (!IS_WHITESPACE(*pos)) break;
		*pos-- = 0;
	}

	return par;
}

static void load_flt_pres(const char *str, int type)
{
	char *arg = get_preset_arg(str);
	if (arg[0])
	{
		if (!strcasecmp(arg, "same") || !strcasecmp(arg, "off"))
		{
			video_apply_scaler_flt(type, 0);
		}
		else
		{
			video_apply_scaler_coeff(type, arg);
			video_apply_scaler_flt(type, 1);
		}
	}
}

void video_loadPreset(char *name, bool save)
{
	char *arg;
	fileTextReader reader;

	bool scaler_dirty = false;
	bool mask_dirty = false;
	bool gamma_dirty = false;

	if (FileOpenTextReader(&reader, name))
	{
		const char *line;
		while ((line = FileReadLine(&reader)))
		{
			if (!strncasecmp(line, "hfilter=", 8))
			{
				load_flt_pres(line + 8, VFILTER_HORZ);
				scaler_dirty = true;
			}
			else if (!strncasecmp(line, "vfilter=", 8))
			{
				load_flt_pres(line + 8, VFILTER_VERT);
				scaler_dirty = true;
			}
			else if (!strncasecmp(line, "sfilter=", 8))
			{
				load_flt_pres(line + 8, VFILTER_SCAN);
				scaler_dirty = true;
			}
			else if (!strncasecmp(line, "ifilter=", 8))
			{
				load_flt_pres(line + 8, VFILTER_ILACE);
				scaler_dirty = true;
			}
			else if (!strncasecmp(line, "mask=", 5))
			{
				mask_dirty = true;
				arg = get_preset_arg(line + 5);
				if (arg[0])
				{
					if (!strcasecmp(arg, "off") || !strcasecmp(arg, "none")) video_apply_shadow_mask_mode(0);
					else video_apply_shadow_mask(arg);
				}
			}
			else if (!strncasecmp(line, "maskmode=", 9))
			{
				mask_dirty = true;
				arg = get_preset_arg(line + 9);
				if (arg[0])
				{
					if (!strcasecmp(arg, "off") || !strcasecmp(arg, "none")) video_apply_shadow_mask_mode(0);
					else if (!strcasecmp(arg, "1x")) video_apply_shadow_mask_mode(SM_MODE_1X);
					else if (!strcasecmp(arg, "2x")) video_apply_shadow_mask_mode(SM_MODE_2X);
					else if (!strcasecmp(arg, "1x rotated")) video_apply_shadow_mask_mode(SM_MODE_1X_ROTATED);
					else if (!strcasecmp(arg, "2x rotated")) video_apply_shadow_mask_mode(SM_MODE_2X_ROTATED);
				}
			}
			else if (!strncasecmp(line, "gamma=", 6))
			{
				gamma_dirty = true;
				arg = get_preset_arg(line + 6);
				if (arg[0])
				{
					if (!strcasecmp(arg, "off") || !strcasecmp(arg, "none")) video_apply_gamma_en(0);
					else
					{
						video_apply_gamma_curve(arg);
						video_apply_gamma_en(1);
					}
				}
			}
		}
	}

	if (save)
	{
		if (scaler_dirty) video_save_scaler_cfg();
		if (mask_dirty) video_save_shadow_mask_cfg();
		if (gamma_dirty) video_save_gamma_cfg();
	}
}

static void hdmi_packet_enable(uint8_t mask, bool enable)
{
	int fd = i2c_open(0x39, 0);
	if (fd >= 0)
	{
		uint8_t packet_val = i2c_smbus_read_byte_data(fd, 0x40);
		if (enable)
			packet_val |= mask;
		else
			packet_val &= ~mask;
		int res = i2c_smbus_write_byte_data(fd, 0x40, packet_val);
		if (res < 0) printf("i2c: write error (%02X %02X): %d\n", 0x40, packet_val, res);
		i2c_close(fd);
	}
}

static void hdmi_packet_set_data(uint8_t mask, uint8_t offset, uint8_t *data, int size)
{
	if (!data)
	{
		hdmi_packet_enable(mask, 0);
		return;
	}

	int fd = i2c_open(0x38, 0);
	if (fd >= 0)
	{
		int res;
		hdmi_packet_enable(mask, 1);

		res = i2c_smbus_write_byte_data(fd, offset + 0x1F, 0x80);
		if (res < 0)
		{
			printf("i2c: Couldn't update packet change register (0x%02X, 0x80) %d\n", offset + 0x1F, res);
		}
		else
		{
			for (int i = 0; i < size; i++)
			{
				res = i2c_smbus_write_byte_data(fd, offset + i, data[i]);
				if (res < 0) printf("i2c: SPD register write error (%02X %02x): %d\n", offset + i, data[i], res);
			}

			res = i2c_smbus_write_byte_data(fd, offset + 0x1F, 0x00);
			if (res < 0) printf("i2c: Couldn't update packet change register (0x%02X, 0x00) %d\n", offset + 0x1F, res);
		}
		i2c_close(fd);
	}
	else
	{
		hdmi_packet_enable(mask, 0);
	}
}

#define hdmi_spd_config(data) hdmi_packet_set_data(0x40, 0x00, data, sizeof(data))

#define hdmi_spare_config(packet, data) hdmi_packet_set_data(packet == 0 ? 0x01 : 0x02, packet == 0 ? 0xC0 : 0xE0, data, sizeof(data))

static void hdmi_config_set_csc()
{
	// default color conversion matrices
	// for the original hexadecimal versions please refer
	// to the ADV7513 programming guide section 4.3.7

	// no transformation, so use identity matrix
	float hdmi_full_coeffs[] = {
		1.0f, 0.0f, 0.0f, 0.0f,
		0.0f, 1.0f, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
	};

	float hdmi_limited_1_coeffs[] = {
		0.8583984375f, 0.0f, 0.0f, 0.06250f,
		0.0f, 0.8583984375f, 0.0f, 0.06250f,
		0.0f, 0.0f, 0.8583984375f, 0.06250f,
		0.0f, 0.0f, 0.0f, 1.0f
	};

	float hdmi_limited_2_coeffs[] = {
		0.93701171875f, 0.0f, 0.0f, 0.06250f,
		0.0f, 0.93701171875f, 0.0f, 0.06250f,
		0.0f, 0.0f, 0.93701171875f, 0.06250f,
		0.0f, 0.0f, 0.0f, 1.0f
	};

	float hdr_dcip3_coeffs[] = {
		0.8225f, 0.1774f, 0.0000f, 0.0f,
		0.0332f, 0.9669f, 0.0000f, 0.0f,
		0.0171f, 0.0724f, 0.9108f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
	};

	const float pi = float(M_PI);

	int ypbpr = (cfg.vga_mode_int == 1) && (cfg.direct_video == 1);

	// out-of-scope defines, not used with ypbpr
	int16_t csc_int16[12];
	int hdmi_limited_1 = cfg.hdmi_limited & 1;
	int hdmi_limited_2 = cfg.hdmi_limited & 2;

	if (!ypbpr)
	{
		// select the base CSC
		int hdr = cfg.hdr;

		mat4x4 coeffs = hdr == 2 ? hdr_dcip3_coeffs : hdmi_full_coeffs;
		mat4x4 csc(coeffs);

		// apply color controls
		float brightness = (((cfg.video_brightness / 100.0f) - 0.5f)); // [-0.5 .. 0.5]
		float contrast = ((cfg.video_contrast / 100.0f) - 0.5f) * 2.0f + 1.0f; // [0 .. 2]
		float saturation = ((cfg.video_saturation / 100.0f)); // [0 .. 1]
		float hue = (cfg.video_hue * pi / 180.0f);

		char* gain_offset = cfg.video_gain_offset;

		// we have to parse these
		float gain_red = 1;
		float gain_green = 1;
		float gain_blue = 1;
		float off_red = 0;
		float off_green = 0;
		float off_blue = 0;

		size_t target = 0;
		float* targets[6] = { &gain_red, &off_red, &gain_green, &off_green, &gain_blue, &off_blue };

		for (size_t i = 0; i < strlen(gain_offset) && target < 6; i++)
		{
			// skip whitespace
			if (gain_offset[i] == ' ' || gain_offset[i] == ',')
				continue;

			int numRead = 0;
			int match = sscanf(gain_offset + i, "%f%n", targets[target], &numRead);

			i += numRead > 0 ? numRead - 1 : 0;

			if (match == 1)
				target++;
		}

		// first apply hue matrix, because it does not touch luminance
		float cos_hue = cos(hue);
		float sin_hue = sin(hue);
		float lr = 0.213f;
		float lg = 0.715f;
		float lb = 0.072f;
		float ca = 0.143f;
		float cb = 0.140f;
		float cc = 0.283f;

		mat4x4 mat_hue;
		mat_hue.setIdentity();

		mat_hue.m11 = lr + cos_hue * (1 - lr) + sin_hue * (-lr);
		mat_hue.m12 = lg + cos_hue * (-lg) + sin_hue * (-lg);
		mat_hue.m13 = lb + cos_hue * (-lb) + sin_hue * (1 - lb);

		mat_hue.m21 = lr + cos_hue * (-lr) + sin_hue * (ca);
		mat_hue.m22 = lg + cos_hue * (1 - lg) + sin_hue * (cb);
		mat_hue.m23 = lb + cos_hue * (-lb) + sin_hue * (cc);

		mat_hue.m31 = lr + cos_hue * (-lr) + sin_hue * (-(1 - lr));
		mat_hue.m32 = lg + cos_hue * (-lg) + sin_hue * (lg);
		mat_hue.m33 = lb + cos_hue * (1 - lb) + sin_hue * (lb);

		csc = csc * mat_hue;

		// now saturation
		float s = saturation;
		float sr = (1.0f - s) * .3086f;
		float sg = (1.0f - s) * .6094f;
		float sb = (1.0f - s) * .0920f;

		float mat_saturation[] = {
			sr + s, sg, sb, 0,
			sr, sg + s, sb, 0,
			sr, sg, sb + s, 0,
			0, 0, 0, 1.0f
		};

		csc = csc * mat4x4(mat_saturation);

		// now brightness and contrast
		float b = brightness;
		float c = contrast;
		float t = (1.0f - c) / 2.0f;

		float mat_brightness_contrast[] = {
			c, 0, 0, (t + b),
			0, c, 0, (t + b),
			0, 0, c, (t + b),
			0, 0, 0, 1.0f
		};

		csc = csc * mat4x4(mat_brightness_contrast);

		// gain and offset
		float rg = gain_red;
		float ro = off_red;
		float gg = gain_green;
		float go = off_green;
		float bg = gain_blue;
		float bo = off_blue;

		float mat_gain_off[] = {
			rg, 0, 0, ro,
			0, gg, 0, go,
			0, 0, bg, bo,
			0, 0, 0, 1.0f
		};

		csc = csc * mat4x4(mat_gain_off);

		// final compression
		csc.compress(2.0f);

		// make sure to retain hdmi limited range
		if (hdmi_limited_1)
			csc = csc * mat4x4(hdmi_limited_1_coeffs);
		else if (hdmi_limited_2)
			csc = csc * mat4x4(hdmi_limited_2_coeffs);

		// finally, apply a fixed multiplier to get it in
		// correct range for ADV7513 chip
		for (size_t i = 0; i < 12; i++)
		{
			csc_int16[i] = int16_t(csc.comp[i] * 2048.0f);
		}
	}
	// Clamps to reinforce limited if necessary
	// 0x100 = 16/256 * 4096 (12-bit mul)
	// 0xEB0 = 235/256 * 4096
	// 0xFFF = 4095 (12-bit max)
	uint16_t clipMin = (!ypbpr && (hdmi_limited_1 || hdmi_limited_2)) ? 0x100 : 0x000;
	uint16_t clipMax = (!ypbpr && hdmi_limited_1) ? 0xEB0 : 0xFFF;

	// pass to HDMI, use 0xA0 to set a mode of [-2 .. 2] per ADV7513 programming guide
	uint8_t csc_data[] = {
		0x18, (uint8_t)(ypbpr ? 0x86 : (0b10100000 | (((csc_int16[0] >> 8) & 0b00011111)))),  // csc Coefficients, Channel A
		0x19, (uint8_t)(ypbpr ? 0xDF : (csc_int16[0] & 0xff)),
		0x1A, (uint8_t)(ypbpr ? 0x1A : (csc_int16[1] >> 8)),
		0x1B, (uint8_t)(ypbpr ? 0x3F : (csc_int16[1] & 0xff)),
		0x1C, (uint8_t)(ypbpr ? 0x1E : (csc_int16[2] >> 8)),
		0x1D, (uint8_t)(ypbpr ? 0xE2 : (csc_int16[2] & 0xff)),
		0x1E, (uint8_t)(ypbpr ? 0x07 : (csc_int16[3] >> 8)),
		0x1F, (uint8_t)(ypbpr ? 0xE7 : (csc_int16[3] & 0xff)),

		0x20, (uint8_t)(ypbpr ? 0x04 : (csc_int16[4] >> 8)),  // csc Coefficients, Channel B
		0x21, (uint8_t)(ypbpr ? 0x1C : (csc_int16[4] & 0xff)),
		0x22, (uint8_t)(ypbpr ? 0x08 : (csc_int16[5] >> 8)),
		0x23, (uint8_t)(ypbpr ? 0x11 : (csc_int16[5] & 0xff)),
		0x24, (uint8_t)(ypbpr ? 0x01 : (csc_int16[6] >> 8)),
		0x25, (uint8_t)(ypbpr ? 0x91 : (csc_int16[6] & 0xff)),
		0x26, (uint8_t)(ypbpr ? 0x01 : (csc_int16[7] >> 8)),
		0x27, (uint8_t)(ypbpr ? 0x00 : (csc_int16[7] & 0xff)),

		0x28, (uint8_t)(ypbpr ? 0x1D : (csc_int16[8] >> 8)),  // csc Coefficients, Channel C
		0x29, (uint8_t)(ypbpr ? 0xAE : (csc_int16[8] & 0xff)),
		0x2A, (uint8_t)(ypbpr ? 0x1B : (csc_int16[9] >> 8)),
		0x2B, (uint8_t)(ypbpr ? 0x73 : (csc_int16[9] & 0xff)),
		0x2C, (uint8_t)(ypbpr ? 0x06 : (csc_int16[10] >> 8)),
		0x2D, (uint8_t)(ypbpr ? 0xDF : (csc_int16[10] & 0xff)),
		0x2E, (uint8_t)(ypbpr ? 0x07 : (csc_int16[11] >> 8)),
		0x2F, (uint8_t)(ypbpr ? 0xE7 : (csc_int16[11] & 0xff)),

		0xC0, (uint8_t)(clipMin >> 8), // HDMI limited clamps
		0xC1, (uint8_t)(clipMin & 0xff),
		0xC2, (uint8_t)(clipMax >> 8),
		0xC3, (uint8_t)(clipMax & 0xff)
	};

	int fd = i2c_open(0x39, 0);
	if (fd >= 0)
	{
		for (uint i = 0; i < sizeof(csc_data); i += 2)
		{
			int res = i2c_smbus_write_byte_data(fd, csc_data[i], csc_data[i + 1]);
			if (res < 0) printf("i2c: write error (%02X %02X): %d\n", csc_data[i], csc_data[i + 1], res);
		}

		i2c_close(fd);
	}
	else
	{
		printf("*** ADV7513 not found on i2c bus! HDMI won't be available!\n");
	}
}

static void hdmi_config_init()
{
	int ypbpr = (cfg.vga_mode_int == 1) && (cfg.direct_video == 1);

	// address, value
	uint8_t init_data[] = {
		0x98, 03,				// ADI required Write.

		0xD6, 0b11000000,		// [7:6] HPD Control...
								// 00 = HPD is from both HPD pin or CDC HPD
								// 01 = HPD is from CDC HPD
								// 10 = HPD is from HPD pin
								// 11 = HPD is always high

		0x41, 0x10,				// Power Down control
		0x9A, 0x70,				// ADI required Write.
		0x9C, 0x30,				// ADI required Write.
		0x9D, 0b01100001,		// [7:4] must be b0110!.
								// [3:2] b00 = Input clock not divided. b01 = Clk divided by 2. b10 = Clk divided by 4. b11 = invalid!
								// [1:0] must be b01!
		0xA2, 0xA4,				// ADI required Write.
		0xA3, 0xA4,				// ADI required Write.
		0xE0, 0xD0,				// ADI required Write.


		0x35, 0x40,
		0x36, 0xD9,
		0x37, 0x0A,
		0x38, 0x00,
		0x39, 0x2D,
		0x3A, 0x00,

		0x16, 0b00111000,		// Output Format 444 [7]=0.
								// [6] must be 0!
								// Colour Depth for Input Video data [5:4] b11 = 8-bit.
								// Input Style [3:2] b10 = Style 1 (ignored when using 444 input).
								// DDR Input Edge falling [1]=0 (not using DDR atm).
								// Output Colour Space RGB [0]=0.

		0x17, 0b01100010,		// Aspect ratio 16:9 [1]=1, 4:3 [1]=0, invert sync polarity

		0x3B, 0x80,             // Automatic pixel repetition and VIC detection
		0x3C, 0x00,

		0x48, 0b00001000,       // [6]=0 Normal bus order!
								// [5] DDR Alignment.
								// [4:3] b01 Data right justified (for YCbCr 422 input modes).

		0x49, 0xA8,				// ADI required Write.
		0x40, 0x00,
		0x4A, 0b10000000,		//Auto-Calculate SPD checksum
		0x4C, 0x00,				// ADI required Write.

		0x55, (uint8_t)(cfg.hdmi_game_mode ? 0b00010010 : 0b00010000),
								// [7] must be 0!. Set RGB444 in AVinfo Frame [6:5], Set active format [4].
								// AVI InfoFrame Valid [4].
								// Bar Info [3:2] b00 Bars invalid. b01 Bars vertical. b10 Bars horizontal. b11 Bars both.
								// Scan Info [1:0] b00 (No data). b01 TV. b10 PC. b11 None.

		0x56, (uint8_t)( 0b00001000 | (cfg.hdr ? 0xb11000000 : 0)),		// [5:4] Picture Aspect Ratio
								// [3:0] Active Portion Aspect Ratio b1000 = Same as Picture Aspect Ratio

		0x57, (uint8_t)((cfg.hdmi_game_mode ? 0x80 : 0x00)		// [7] IT Content. 0 - No. 1 - Yes (type set in register 0x59).
																// [6:4] Color space (ignored for RGB)
			| ((ypbpr || cfg.hdmi_limited) ? 0b0100 : cfg.hdr ? 0b1101000 : 0b0001000)),	// [3:2] RGB Quantization range
																// [1:0] Non-Uniform Scaled: 00 - None. 01 - Horiz. 10 - Vert. 11 - Both.

		0x59, (uint8_t)(cfg.hdmi_game_mode ? 0x30 : 0x00),		// [7:6] [YQ1 YQ0] YCC Quantization Range: b00 = Limited Range, b01 = Full Range
																// [5:4] IT Content Type b11 = Game, b00 = Graphics/None
																// [3:0] Pixel Repetition Fields b0000 = No Repetition

		0x73, 0x01,

		0x94, 0b10000000,       // [7]=1 HPD Interrupt ENabled.

		0x99, 0x02,				// ADI required Write.
		0x9B, 0x18,				// ADI required Write.

		0x9F, 0x00,				// ADI required Write.

		0xA1, 0b00000000,	    // [6]=1 Monitor Sense Power Down DISabled.

		0xA4, 0x08,				// ADI required Write.
		0xA5, 0x04,				// ADI required Write.
		0xA6, 0x00,				// ADI required Write.
		0xA7, 0x00,				// ADI required Write.
		0xA8, 0x00,				// ADI required Write.
		0xA9, 0x00,				// ADI required Write.
		0xAA, 0x00,				// ADI required Write.
		0xAB, 0x40,				// ADI required Write.

		0xAF, (uint8_t)(0b00000100	// [7]=0 HDCP Disabled.
								// [6:5] must be b00!
								// [4]=0 Current frame is unencrypted
								// [3:2] must be b01!
			| ((cfg.dvi_mode == 1) ? 0b00 : 0b10)),	 //	[1]=1 HDMI Mode.
								// [0] must be b0!

		0xB9, 0x00,				// ADI required Write.

		0xBA, 0b01100000,		// [7:5] Input Clock delay...
								// b000 = -1.2ns.
								// b001 = -0.8ns.
								// b010 = -0.4ns.
								// b011 = No delay.
								// b100 = 0.4ns.
								// b101 = 0.8ns.
								// b110 = 1.2ns.
								// b111 = 1.6ns.

		0xBB, 0x00,				// ADI required Write.
		0xDE, 0x9C,				// ADI required Write.
		0xE4, 0x60,				// ADI required Write.
		0xFA, 0x7D,				// Nbr of times to search for good phase

		// (Audio stuff on Programming Guide, Page 66)...
		0x0A, 0b00000000,		// [6:4] Audio Select. b000 = I2S.
								// [3:2] Audio Mode. (HBR stuff, leave at 00!).

		0x0B, 0b00001110,		//

		0x0C, 0b00000100,		// [7] 0 = Use sampling rate from I2S stream.   1 = Use samp rate from I2C Register.
								// [6] 0 = Use Channel Status bits from stream. 1 = Use Channel Status bits from I2C register.
								// [2] 1 = I2S0 Enable.
								// [1:0] I2S Format: 00 = Standard. 01 = Right Justified. 10 = Left Justified. 11 = AES.

		0x0D, 0b00010000,		// [4:0] I2S Bit (Word) Width for Right-Justified.
		0x14, 0b00000010,		// [3:0] Audio Word Length. b0010 = 16 bits.
		0x15, (uint8_t)((cfg.hdmi_audio_96k ? 0x80 : 0x00) | 0b0100000),	// I2S Sampling Rate [7:4]. b0000 = (44.1KHz). b0010 = 48KHz.
								// Input ID [3:1] b000 (0) = 24-bit RGB 444 or YCrCb 444 with Separate Syncs.

		// Audio Clock Config
		0x01, 0x00,				//
		0x02, (uint8_t)(cfg.hdmi_audio_96k ? 0x30 : 0x18),	// Set N Value 12288/6144
		0x03, 0x00,				//

		0x07, 0x01,				//
		0x08, 0x22,				// Set CTS Value 74250
		0x09, 0x0A,				//
	};

	int fd = i2c_open(0x39, 0);
	if (fd >= 0)
	{
		for (uint i = 0; i < sizeof(init_data); i += 2)
		{
			int res = i2c_smbus_write_byte_data(fd, init_data[i], init_data[i + 1]);
			if (res < 0) printf("i2c: write error (%02X %02X): %d\n", init_data[i], init_data[i + 1], res);
		}

		i2c_close(fd);
	}
	else
	{
		printf("*** ADV7513 not found on i2c bus! HDMI won't be available!\n");
	}

	hdmi_config_set_csc();
}

void video_hdmi_power(int on)
{
	// ADV7513 power-down control. 0 = power on, 1 = power down.
	int fd = i2c_open(0x39, 0);
	if (fd >= 0)
	{
		uint8_t val = on ? 0x00 : 0x40;
		int res = i2c_smbus_write_byte_data(fd, 0x41, val);
		if (res < 0) printf("i2c: write error (41 %02X): %d\n", val, res);
		i2c_close(fd);
	}
	else
	{
		printf("*** ADV7513 not found on i2c bus! HDMI won't be available!\n");
	}
}

static void hdmi_config_set_hdr()
{
	// Grab desired nits values
	uint8_t maxNitsLSB = cfg.hdr_max_nits & 0xFF;
	uint8_t maxNitsMSB = (cfg.hdr_max_nits >> 8) & 0xFF;

	uint8_t avgNitsLSB = cfg.hdr_avg_nits & 0xFF;
	uint8_t avgNitsMSB = (cfg.hdr_avg_nits >> 8) & 0xFF;

	// CTA-861-G: 6.9 Dynamic Range and Mastering InfoFrame
	// Uses BT2020 RGB primaries and white point chromacity
	// Max Lum: 1000cd/m2, Min Lum: 0cd/m2, MaxCLL: 1000cd/m2
	// MaxFALL: 250cd/m2 (this value does not matter much -
	// in essence it means that the display should expect -
	// 25% of the image to be 1000cd/m2)
	// If HDR == 1, use HLG
	uint8_t hdr_data[] = {
		0x87,
		0x01,
		0x1a,
		0x00, // Checksum, calculate later
		(cfg.hdr == 1 ? uint8_t(0x03) : uint8_t(0x02)),
		0x48,
		0x8a,
		0x08,
		0x39,
		0x34,
		0x21,
		0xaa,
		0x9b,
		0x96,
		0x19,
		0xfc,
		0x08,
		0x13,
		0x3d,
		0x42,
		0x40,
		0x00,
		maxNitsLSB,
		maxNitsMSB,
		0x01,
		0x00,
		maxNitsLSB,
		maxNitsMSB,
		avgNitsLSB,
		avgNitsMSB
	};

	// now we calculate the checksum for this packet (2s complement sum)
	uint8_t checksum = 0;
	for (uint i = 0; i < sizeof(hdr_data); i++) checksum += hdr_data[i];
	hdr_data[3] = ~checksum + 1;

	if (cfg.hdr == 0)
	{
		hdmi_spare_config(1, 0);
	}
	else
	{
		hdmi_spare_config(1, hdr_data);
	}
}

static uint8_t last_sync_invert = 0xff;
static uint8_t last_pr_flags = 0xff;
static uint8_t last_vic_mode = 0xff;

static void hdmi_config_set_mode(vmode_custom_t *vm)
{
	PROFILE_FUNCTION();

	const uint8_t vic_mode = (uint8_t)vm->param.vic;
	uint8_t pr_flags;

	if (cfg.direct_video && is_menu()) pr_flags = 0; // automatic pixel repetition
	else if (vm->param.pr != 0) pr_flags = 0b01001000; // manual pixel repetition with 2x clock
	else pr_flags = 0b01000000; // manual pixel repetition

	uint8_t sync_invert = 0;
	if (vm->param.hpol == 0) sync_invert |= 1 << 5;
	if (vm->param.vpol == 0) sync_invert |= 1 << 6;

	if (last_sync_invert == sync_invert && last_pr_flags == pr_flags && last_vic_mode == vic_mode) return;

	// address, value
	uint8_t init_data[] = {
		0x17, (uint8_t)(0b00000010 | sync_invert),		// Aspect ratio 16:9 [1]=1, 4:3 [1]=0
		0x3B, pr_flags,
		0x3C, vic_mode,			// VIC
	};

	int fd = i2c_open(0x39, 0);
	if (fd >= 0)
	{
		for (uint i = 0; i < sizeof(init_data); i += 2)
		{
			int res = i2c_smbus_write_byte_data(fd, init_data[i], init_data[i + 1]);
			if (res < 0) printf("i2c: write error (%02X %02X): %d\n", init_data[i], init_data[i + 1], res);
		}

		i2c_close(fd);
	}
	else
	{
		printf("*** ADV7513 not found on i2c bus! HDMI won't be available!\n");
	}

	last_pr_flags = pr_flags;
	last_sync_invert = sync_invert;
	last_vic_mode = vic_mode;
}

static void edid_parse_cea_ext(uint8_t *cea)
{
	uint8_t *data_block_end = cea + cea[2];
	uint8_t *cur_blk_start = cea + 4;
	uint8_t *cur_blk_data = cur_blk_start;
	while (cur_blk_start != data_block_end)
	{
		cur_blk_data = cur_blk_start;
		uint8_t blk_tag = (*cur_blk_data & 0xe0) >> 5;
		uint8_t blk_size = *cur_blk_data & 0x1f;
		uint8_t blk_data_size = blk_size; //size of actual data in the block, it might be adjusted if the first byte is extended tag
		cur_blk_data++;
		//vendor specific block might be the only one?

		uint8_t is_vendor_specific = 0;
		if (blk_tag == 0x03) is_vendor_specific = 1;
		if (blk_tag == 0x07)
		{
			if (*cur_blk_data == 0x01) is_vendor_specific = 1;
			cur_blk_data++; //The extended tag uses the next byte for the type. We may not need it?
			blk_data_size--;
		}

		if (is_vendor_specific && blk_data_size >= 3)
		{
			int oui = cur_blk_data[0] | cur_blk_data[1] << 8 | cur_blk_data[2] << 16;
			cur_blk_data += 3;
			blk_data_size -= 3;
			if (oui == 0x00001a) //AMD block
			{
				uint8_t min_fr = cur_blk_data[2];

				uint8_t max_fr = cur_blk_data[3];
				if (max_fr > 62) max_fr = 62;
				if (min_fr && max_fr)
				{
					vrr_modes[VRR_FREESYNC].available = 1;
					vrr_modes[VRR_FREESYNC].min_fr = min_fr;
					vrr_modes[VRR_FREESYNC].max_fr = max_fr;
				}
			}
			else if (oui == 0xc45dd8)
			{
				if (blk_data_size > 5) //VRR lies beyond here
				{
					uint8_t min_fr = cur_blk_data[5] & 0x3f;
					uint8_t max_fr = (cur_blk_data[5] & 0xc0) << 2 | cur_blk_data[6];
					if (max_fr > 62) max_fr = 62;
					if (min_fr && max_fr)
					{
						vrr_modes[VRR_VESA].available = 1;
						vrr_modes[VRR_VESA].min_fr = min_fr;
						vrr_modes[VRR_VESA].max_fr = max_fr;
					}
				}
			}
		}
		cur_blk_start += blk_size + 1;
	}
}

static int find_edid_vrr_capability()
{
	uint8_t *cur_ext = NULL;
	uint8_t ext_cnt = edid[126];

	//Probably only one extension, but just in case...
	for (int i = 0; i < ext_cnt; i++)
	{
		cur_ext = edid + 128 + i * 128; //edid extension blocks are 128 bytes
		uint8_t ext_tag = *cur_ext;
		if (ext_tag == 0x02) //CEA EDID extension
		{
			edid_parse_cea_ext(cur_ext);
		}
	}

	for (size_t i = 1; i < sizeof(vrr_modes) / sizeof(vrr_cap_t); i++)
	{
		if (vrr_modes[i].available) printf("VRR: %s available\n", vrr_modes[i].description);
	}
	return 0;

}

static int is_edid_valid()
{
	static const uint8_t magic[] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 };
	if (sizeof(edid) < sizeof(magic)) return 0;
	return !memcmp(edid, magic, sizeof(magic));
}

static int get_active_edid()
{
	int fd = i2c_open(0x39, 0);
	if (fd < 0)
	{
		printf("EDID: cannot find main i2c device\n");
		return 0;
	}

	//Test if adv7513 senses hdmi clock. If not, don't bother with the edid query
	int hpd_state = i2c_smbus_read_byte_data(fd, 0x42);
	if (hpd_state < 0 || !(hpd_state & 0x20))
	{
		i2c_close(fd);
		return 0;
	}


	for (int i = 0; i < 10; i++)
	{
		i2c_smbus_write_byte_data(fd, 0xC9, 0x03);
		i2c_smbus_write_byte_data(fd, 0xC9, 0x13);
	}
	i2c_close(fd);
	fd = i2c_open(0x3f, 0);
	if (fd < 0)
	{
		printf("EDID: cannot find i2c device.\n");
		return 0;
	}

	// waiting for valid EDID
	for (int k = 0; k < 20; k++)
	{
		for (uint i = 0; i < sizeof(edid); i++) edid[i] = (uint8_t)i2c_smbus_read_byte_data(fd, i);
		if (is_edid_valid()) break;
		usleep(100000);
	}

	i2c_close(fd);
	printf("EDID:\n"); hexdump(edid, sizeof(edid), 0);

	if (!is_edid_valid())
	{
		printf("Invalid EDID: incorrect header.\n");
		bzero(edid, sizeof(edid));
		return 0;
	}
	return 1;
}

static int get_edid_vmode(vmode_custom_t *v)
{
	if (!is_edid_valid())
	{
		get_active_edid();
	}

	if (!is_edid_valid()) return 0;

	int hact, vact, pixclk_khz, hfp, hsync, hbp, vfp, vsync, vbp, hbl, vbl;
	uint8_t *x = edid + 0x36;

	pixclk_khz = (x[0] + (x[1] << 8)) * 10;
	if (pixclk_khz < 10000)
	{
		if (!pixclk_khz) printf("Invalid EDID: First two bytes are 0, invalid data.\n");
		else printf("Invalid EDID: Pixelclock < 10 MHz, assuming invalid data 0x%02x 0x%02x.\n", x[0], x[1]);
		return 0;
	}

	if (cfg.dvi_mode == 2)
	{
		cfg.dvi_mode = (edid[0x80] == 2 && edid[0x81] == 3 && (edid[0x83] & 0x40)) ? 0 : 1;
		if (cfg.dvi_mode == 1) printf("EDID: using DVI mode.\n");
	}

	unsigned char flags = x[17];
	if (flags & 0x80)
	{
		printf("EDID: preferred mode is interlaced. Fall back to default video mode.\n");
		return 0;
	}

	hact = (x[2] + ((x[4] & 0xf0) << 4));
	hbl = (x[3] + ((x[4] & 0x0f) << 8));
	hfp = (x[8] + ((x[11] & 0xc0) << 2));
	hsync = (x[9] + ((x[11] & 0x30) << 4));
	hbp = hbl - hsync - hfp;
	vact = (x[5] + ((x[7] & 0xf0) << 4));
	vbl = (x[6] + ((x[7] & 0x0f) << 8));
	vfp = ((x[10] >> 4) + ((x[11] & 0x0c) << 2));
	vsync = ((x[10] & 0x0f) + ((x[11] & 0x03) << 4));
	vbp = vbl - vsync - vfp;

	/*
	int pos_pol_hsync = 0;
	int pos_pol_vsync = 0;
	int no_pol_vsync = 0; // digital composite signals have no vsync polarity

	switch ((flags & 0x18) >> 3)
	{
	case 0x02:
		if (flags & (1 << 1)) pos_pol_hsync = 1;
		no_pol_vsync = 1;
		break;
	case 0x03:
		if (flags & (1 << 1)) pos_pol_hsync = 1;
		if (flags & (1 << 2)) pos_pol_vsync = 1;
		break;
	}
	*/

	double Fpix = pixclk_khz / 1000.f;
	double frame_rate = Fpix * 1000000.f / ((hact + hfp + hbp + hsync)*(vact + vfp + vbp + vsync));
	printf("EDID: preferred mode: %dx%d@%.1f, pixel clock: %.3fMHz\n", hact, vact, frame_rate, Fpix);

	if (hact >= 1920) support_FHD = 1;

	if (hact > 2048)
	{
		printf("EDID: Preferred resolution is too high (%dx%d).\n", hact, vact);
		printf("EDID: Falling back to default video mode.\n");
		return 0;
	}

	memset(v, 0, sizeof(vmode_custom_t));
	v->item[1] = hact;
	v->item[2] = hfp;
	v->item[3] = hsync;
	v->item[4] = hbp;
	v->item[5] = vact;
	v->item[6] = vfp;
	v->item[7] = vsync;
	v->item[8] = vbp;
	v->Fpix = Fpix;

	if (Fpix > 210.f)
	{
		printf("EDID: Preferred mode has too high pixel clock (%.3fMHz).\n", Fpix);
		if (hact == 2048 && vact == 1536)
		{
			int n = 13;
			printf("EDID: Using safe vmode %d.\n", n);
			for (int i = 0; i < 8; i++) v->item[i + 1] = vmodes[n].vpar[i];
			v->param.vic = vmodes[n].vic_mode;
			v->Fpix = vmodes[n].Fpix;
		}
		else
		{
			int fail = 1;
			if (frame_rate > 60.f)
			{
				Fpix = 60.f * (hact + hfp + hbp + hsync)*(vact + vfp + vbp + vsync) / 1000000.f;
				if (Fpix <= 210.f)
				{
					printf("EDID: Reducing frame rate to 60Hz with new pixel clock %.3fMHz.\n", Fpix);
					v->Fpix = Fpix;
					fail = 0;
				}
			}

			if (fail)
			{
				printf("EDID: Falling back to default video mode.\n");
				return 0;
			}
		}
	}

	v->param.rb = 2;
	setPLL(v->Fpix, v);
	return 1;
}

static void set_vrr_mode()
{
	PROFILE_FUNCTION();

	use_vrr = 0;
	float vrateh = 100000000;

	if (cfg.vrr_mode == 0)
	{
		if (last_vrr_mode != 0)
		{
			hdmi_spd_config(0);
			hdmi_spare_config(0, 0);
		}
		last_vrr_mode = 0;
		use_freesync_spd = 0;
		return;
	}

	if (current_video_info.vtimeh) vrateh /= current_video_info.vtimeh; else vrateh = 0;
	if (cfg.vrr_vesa_framerate) vrateh = cfg.vrr_vesa_framerate;

	if ((last_vrr_mode == cfg.vrr_mode) &&
		(last_vrr_rate == vrateh) &&
		(last_vrr_vfp == v_cur.param.vfp || cfg.vrr_mode != VRR_VESA)) return;

	if (!is_edid_valid())
	{
		get_active_edid();
	}

	if (!is_edid_valid())
	{
		printf("Set VRR: No valid edid, cannot set\n");
		return;
	}

	find_edid_vrr_capability();

	if (cfg.vrr_mode == 1) //autodetect
	{
		for (uint8_t i = 1; i < sizeof(vrr_modes) / sizeof(vrr_cap_t); i++)
		{
			if (vrr_modes[i].available)
			{
				use_vrr = i;
				break;
			}
		}
	}
	else if (cfg.vrr_mode == 2)
	{ //force AMD Freesync
		use_vrr = VRR_FREESYNC;
	}
	else if (cfg.vrr_mode == 3)
	{ //force Vesa Forum VRR
		use_vrr = VRR_VESA;
	}
	else
	{
		use_vrr = 0;
	}

	vrr_min_fr = 0;
	vrr_max_fr = 0;

	if (use_vrr == VRR_VESA && !vrateh) return;
	if (use_vrr)
	{
		vrr_min_fr = cfg.vrr_min_framerate;
		vrr_max_fr = cfg.vrr_max_framerate;

		if (!vrr_min_fr) vrr_min_fr = vrr_modes[use_vrr].min_fr;
		if (!vrr_max_fr) vrr_max_fr = vrr_modes[use_vrr].max_fr;

		if (!vrr_min_fr) vrr_min_fr = 47;
		if (!vrr_max_fr) vrr_max_fr = 62;

		vrr_modes[use_vrr].active = 1;
		printf("VRR: Set %s active\n", vrr_modes[use_vrr].description);
		if (use_vrr == VRR_VESA)
		{
			printf("VESA Frame Rate %d Front Porch %d\n", (int)vrateh, v_cur.param.vfp);
		}
	}

	int16_t vrateh_i = (int16_t)vrateh;

	if (use_vrr == VRR_FREESYNC)
	{
		uint8_t freesync_data[] = {
			//header
			0x83, 0x01, 0x08, 0x00,
			//data
			0x1A, 0x00, 0x00, 0x00, 0x00, 0x07,
			vrr_min_fr,
			vrr_max_fr,
		};

		use_freesync_spd = 1;
		hdmi_spd_config(freesync_data);
	}
	else if(use_freesync_spd)
	{
		use_freesync_spd = 0;
		hdmi_spd_config(0);
	}

	if (use_vrr == VRR_VESA)
	{
		uint8_t vesa_data[] = {
			0x7F, 0xC0, 0x00,
			0x40, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04,
			0x01,
			(uint8_t)v_cur.param.vfp,
			(uint8_t)((vrateh_i >> 8) & 0x03),
			(uint8_t)(vrateh_i & 0xFF),
		};

		hdmi_spare_config(0, vesa_data);
	}
	else
	{
		hdmi_spare_config(0, 0);
	}

	last_vrr_mode = cfg.vrr_mode;
	last_vrr_rate = vrateh;
	last_vrr_vfp = v_cur.param.vfp;

	if (!supports_vrr() || cfg.vsync_adjust) use_vrr = 0;
}

static void video_set_mode(vmode_custom_t *v, double Fpix)
{
	PROFILE_FUNCTION();

	setGamma();
	setScaler();

	v_cur = *v;
	vmode_custom_t v_fix = v_cur;
	if (cfg.direct_video)
	{
		v_fix.item[2] = FB_DV_RBRD;
		v_fix.item[4] = FB_DV_LBRD;
		v_fix.item[1] += v_cur.item[2] - v_fix.item[2];
		v_fix.item[1] += v_cur.item[4] - v_fix.item[4];

		v_fix.item[6] = FB_DV_BBRD;
		v_fix.item[8] = FB_DV_UBRD;;
		v_fix.item[5] += v_cur.item[6] - v_fix.item[6];
		v_fix.item[5] += v_cur.item[8] - v_fix.item[8];
	}
	else
	{
		set_vrr_mode();
	}

	if (Fpix) setPLL(Fpix, &v_cur);
	if (use_vrr)
	{
		printf("Requested variable refresh rate: min=%dHz, max=%dHz\n", vrr_min_fr, vrr_max_fr);
		int horz = v_fix.param.hact + v_fix.param.hbp + v_fix.param.hfp + v_fix.param.hs;

#if 0
		// variant 1: try to reduce vblank to reach max refresh rate but keep original pixel clock.
		// try to adjust VBlank to match max refresh
		int vbl_fmax = ((v_cur.Fpix * 1000000.f) / (vrr_max_fr * horz)) - v_fix.param.vact - v_fix.param.vs - 1;
		if (vbl_fmax < 2) vbl_fmax = 2;
		int vfp = vbl_fmax - v_fix.param.vbp;
		v_fix.param.vfp = vfp;
		if (vfp < 1)
		{
			v_fix.param.vfp = 1;
			v_fix.param.vbp = vbl_fmax - 1;
		}
		int vert = v_fix.param.vact + v_fix.param.vbp + v_fix.param.vfp + v_fix.param.vs;
#else
		// variant 2: keep original vblank and adjust pixel clock to max refresh rate
		int vert = v_fix.param.vact + v_fix.param.vbp + v_fix.param.vfp + v_fix.param.vs;
		Fpix = horz * vert * vrr_max_fr;
		Fpix /= 1000000.f;
		setPLL(Fpix, &v_cur);
#endif

		double freq_max = (v_cur.Fpix * 1000000.f) / (horz * vert);
		double freq_min = vrr_min_fr;
		int vfp_vrr = 0;
		if (freq_min && freq_min < freq_max)
		{
			vfp_vrr = ((v_cur.Fpix * 1000000.f) / (vrr_min_fr * horz)) - vert + 1;
			v_fix.param.vfp += vfp_vrr;
			if (v_fix.param.vfp > 4095) v_fix.param.vfp = 4095;
		}

		vert = v_fix.param.vact + v_fix.param.vbp + v_fix.param.vfp + v_fix.param.vs;
		freq_min = (v_cur.Fpix * 1000000.f) / (horz * vert);
		printf("Using variable refresh rate: min=%2.1fHz, max=%2.1fHz. Additional VFP lines: %d\n", freq_min, freq_max, vfp_vrr);
	}

	printf("Send HDMI parameters:\n");
	spi_uio_cmd_cont(UIO_SET_VIDEO);
	printf("video: ");
	for (int i = 1; i <= 8; i++)
	{
		if (i == 1) spi_w((v_cur.param.pr << 15) | ((use_vrr ? 1 : 0) << 14) | v_fix.item[i]);
		//hsync polarity
		else if (i == 3) spi_w((!!v_cur.param.hpol << 15) | v_fix.item[i]);
		//vsync polarity
		else if (i == 7) spi_w((!!v_cur.param.vpol << 15) | v_fix.item[i]);
		else spi_w(v_fix.item[i]);
		printf("%d(%d), ", v_cur.item[i], v_fix.item[i]);
	}

	printf("%chsync, %cvsync\n", !!v_cur.param.hpol ? '+' : '-', !!v_cur.param.vpol ? '+' : '-');

	printf("PLL: ");
	for (int i = 9; i < 21; i++)
	{
		printf("0x%X, ", v_cur.item[i]);
		if (i & 1) spi_w(v_cur.item[i] | ((i == 9 && Fpix && cfg.vsync_adjust == 2 && !is_menu()) ? 0x8000 : 0) | 0x4000);
		else
		{
			spi_w(v_cur.item[i]);
			spi_w(v_cur.item[i] >> 16);
		}
	}

	printf("Fpix=%f\n", v_cur.Fpix);
	DisableIO();

	hdmi_config_set_mode(&v_cur);

	video_fb_config();

	setShadowMask();
}

static int parse_custom_video_mode(char* vcfg, vmode_custom_t *v)
{
	char *tokens[32];
	uint32_t val[32];
	double valf = 0;

	char work[1024];
	char *next;

	if (!vcfg[0]) return -1;

	memset(v, 0, sizeof(vmode_custom_t));
	v->param.rb = 1; // default reduced blanking to true

	int token_cnt = str_tokenize(strcpyz(work, vcfg), ",", tokens, 32);

	int cnt;
	for (cnt = 0; cnt < token_cnt; cnt++)
	{
		val[cnt] = strtoul(tokens[cnt], &next, 0);
		if (*next)
		{
			break;
		}
	}

	if (cnt == 2 && token_cnt > 2)
	{
		valf = strtod(tokens[cnt], &next);
		if (!*next) cnt++;
	}

	for (int i = cnt; i < token_cnt; i++)
	{
		const char *flag = tokens[i];
		if (!strcasecmp(flag, "+vsync")) v->param.vpol = 1;
		else if (!strcasecmp(flag, "-vsync")) v->param.vpol = 0;
		else if (!strcasecmp(flag, "+hsync")) v->param.hpol = 1;
		else if (!strcasecmp(flag, "-hsync")) v->param.hpol = 0;
		else if (!strcasecmp(flag, "cvt")) v->param.rb = 0;
		else if (!strcasecmp(flag, "cvtrb")) v->param.rb = 1;
		else if (!strcasecmp(flag, "pr")) v->param.pr = 1;
		else
		{
			printf("Error parsing video_mode parameter %d \"%s\": \"%s\"\n", i, flag, vcfg);
			cfg_error("Invalid video_mode\n> %s", vcfg);
			return -1;
		}
	}

	if (cnt == 1)
	{
		v->item[0] = val[0];
		printf("Set predefined video_mode to %d\n", v->item[0]);
		return v->item[0];
	}
	else if (cnt == 3)
	{
		video_calculate_cvt(val[0], val[1], valf ? valf : val[2], v->param.rb, v);
	}
	else if (cnt >= 21)
	{
		for (int i = 0; i < cnt; i++)
			v->item[i] = val[i];
	}
	else if (cnt == 9 || cnt == 11)
	{
		v->item[0] = 1;
		for (int i = 0; i < 8; i++)
			v->item[i + 1] = val[i];

		v->Fpix = val[8] / 1000.0;

		if (cnt == 11)
		{
			v->param.hpol = val[9];
			v->param.vpol = val[10];
		}
	}
	else
	{
		printf("Error parsing video_mode parameter: ""%s""\n", vcfg);
		cfg_error("Invalid video_mode\n> %s", vcfg);
		return -1;
	}

	setPLL(v->Fpix, v);
	return -2;
}

static int store_custom_video_mode(char* vcfg, vmode_custom_t *v)
{
	int ret = parse_custom_video_mode(vcfg, v);
	if (ret == -2) return 1;

	uint mode = (ret >= 0) ? ret : (support_FHD) ? 8 : 0;
	if (mode >= VMODES_NUM) mode = 0;
	if (vmodes[mode].pr == 1 && !supports_pr()) mode = 8;
	for (int i = 0; i < 8; i++) v->item[i + 1] = vmodes[mode].vpar[i];
	v->param.vic = vmodes[mode].vic_mode;
	v->param.pr = vmodes[mode].pr;
	v->param.rb = 1;
	setPLL(vmodes[mode].Fpix, v);

	return ret >= 0;
}

static void fb_init()
{
	if (!fb_base)
	{
		fb_base = (volatile uint32_t*)shmem_map(FB_ADDR, FB_SIZE * 4 * 3);
		if (!fb_base)
		{
			printf("Unable to mmap FB!\n");
		}
	}
	spi_uio_cmd16(UIO_SET_FBUF, 0);
}

// Structure to hold DAC configuration
struct dac_config {
	uint16_t mfg_id;
	uint8_t hdmi_limited;
	uint8_t hdmi_audio_96k;
	int8_t composite_sync; // -1=use MiSTer.ini, 0=force off, 1=force on
	char name[64];
};

static dac_config dac_configs[32];
static int dac_config_count = 0;

static void load_dac_file(const char *filename)
{
	fileTextReader reader;
	const char *line;

	if (!FileOpenTextReader(&reader, filename)) {
		return;
	}

	printf("Loading DAC configuration from %s\n", filename);

	while ((line = FileReadLine(&reader)) && dac_config_count < 32) {
		// Skip comments and empty lines
		if (line[0] == '#' || line[0] == '\n' || line[0] == '\r' || line[0] == '\0') continue;

		// Parse format: mfg_id,hdmi_limited,hdmi_audio_96k,composite_sync,name
		// Example: 0x48F4,0,0,,Full-range CS5213 DAC
		unsigned int mfg, limited, audio_96k = 0;
		char name[64] = {0};
		int8_t csync = -1; // Default to use MiSTer.ini setting

		// Find commas to parse fields manually (to handle empty fields)
		const char *comma1 = strchr(line, ',');
		if (!comma1) continue;
		const char *comma2 = strchr(comma1 + 1, ',');
		if (!comma2) continue;
		const char *comma3 = strchr(comma2 + 1, ',');
		if (!comma3) continue;
		const char *comma4 = strchr(comma3 + 1, ',');
		if (!comma4) continue;

		// Parse mfg_id, hdmi_limited, hdmi_audio_96k
		if (sscanf(line, "%x,%u,%u", &mfg, &limited, &audio_96k) >= 2) {
			// Parse composite_sync field (between comma3 and comma4)
			if (comma4 - comma3 > 1) {
				char csync_char = comma3[1];
				if (csync_char == '0' || csync_char == '1') {
					csync = (csync_char == '1') ? 1 : 0;
				}
			}

			// Parse name (after comma4)
			strncpy(name, comma4 + 1, 63);
			// Remove newline if present
			char *newline = strchr(name, '\n');
			if (newline) *newline = '\0';
			char *carriage = strchr(name, '\r');
			if (carriage) *carriage = '\0';
			// Check if this mfg_id already exists and update it
			bool found = false;
			for (int i = 0; i < dac_config_count; i++) {
				if (dac_configs[i].mfg_id == mfg) {
					dac_configs[i].hdmi_limited = limited;
					dac_configs[i].hdmi_audio_96k = audio_96k;
					dac_configs[i].composite_sync = csync;
					strncpy(dac_configs[i].name, name, 63);
					printf("  Updated DAC: mfg=0x%04X, hdmi_limited=%d, hdmi_audio_96k=%d, csync=%d, %s\n",
						mfg, limited, audio_96k, csync, name);
					found = true;
					break;
				}
			}

			if (!found) {
				dac_configs[dac_config_count].mfg_id = mfg;
				dac_configs[dac_config_count].hdmi_limited = limited;
				dac_configs[dac_config_count].hdmi_audio_96k = audio_96k;
				dac_configs[dac_config_count].composite_sync = csync;
				strncpy(dac_configs[dac_config_count].name, name, 63);
				printf("  DAC[%d]: mfg=0x%04X, hdmi_limited=%d, hdmi_audio_96k=%d, csync=%d, %s\n",
					dac_config_count, mfg, limited, audio_96k, csync, name);
				dac_config_count++;
			}
		}
	}
}

static void load_dac_config()
{
	dac_config_count = 0;

	// Try to load main DAC file
	load_dac_file("dv_dac.txt");

	// Load user DAC file (overrides/adds to main file)
	load_dac_file("dv_dac_user.txt");

	if (dac_config_count == 0) {
		// If no files found, use built-in defaults
		dac_configs[0].mfg_id = 0x48F4;
		dac_configs[0].hdmi_limited = 0;
		dac_configs[0].hdmi_audio_96k = 0;
		dac_configs[0].composite_sync = -1;
		strcpy(dac_configs[0].name, "Full-range DAC (built-in)");

		dac_configs[1].mfg_id = 0x04EF;
		dac_configs[1].hdmi_limited = 2;
		dac_configs[1].hdmi_audio_96k = 0;
		dac_configs[1].composite_sync = -1;
		strcpy(dac_configs[1].name, "Limited-range DAC (built-in)");

		dac_config_count = 2;
		printf("No DAC config files found, using built-in defaults\n");
	}

	printf("Total: %d DAC configurations loaded\n", dac_config_count);
}

static int should_auto_enable_direct_video()
{
	// Load DAC config on first call
	static bool dac_config_loaded = false;
	if (!dac_config_loaded) {
		load_dac_config();
		dac_config_loaded = true;
	}

	// Read EDID if not already valid
	if (!is_edid_valid()) {
		get_active_edid();
	}

	if (!is_edid_valid()) return 0;

	// Check manufacturer ID (bytes 0x08-0x09)
	uint16_t mfg_id = (edid[0x08] << 8) | edid[0x09];

	// Check against known DACs from config
	for (int i = 0; i < dac_config_count; i++) {
		if (mfg_id == dac_configs[i].mfg_id) {
			printf("EDID: Detected known DAC: %s\n", dac_configs[i].name);
			printf("EDID: Auto-enabling direct video with hdmi_limited=%d, hdmi_audio_96k=%d\n",
				dac_configs[i].hdmi_limited, dac_configs[i].hdmi_audio_96k);
			cfg.hdmi_limited = dac_configs[i].hdmi_limited;
			cfg.hdmi_audio_96k = dac_configs[i].hdmi_audio_96k;

			// Set composite sync if specified
			if (dac_configs[i].composite_sync >= 0) {
				cfg.csync = dac_configs[i].composite_sync;
				printf("EDID: Auto-setting composite_sync=%d\n", dac_configs[i].composite_sync);
			} else {
				printf("EDID: Composite sync setting deferred to MiSTer.ini\n");
			}
			return 1;
		}
	}

	// Not a known DAC, don't enable direct video
	return 0;
}

static void video_mode_load()
{
	// Auto-detect and enable direct video if configured
	if (cfg.direct_video == 2) {
		if (should_auto_enable_direct_video()) {
			printf("Auto-enabling direct video for known DAC.\n");
			// Enable direct video, preserve all other user settings
			cfg.direct_video = 1;
		} else {
			// Not a DAC, use normal HDMI mode
			cfg.direct_video = 0;
		}

		// direct_video=2 resolves here, so refresh HDMI CSC with final mode.
		hdmi_config_set_csc();
	}

	if (cfg.direct_video && cfg.vsync_adjust)
	{
		printf("Disabling vsync_adjust because of enabled direct video.\n");
		cfg.vsync_adjust = 0;
	}

	if (cfg.direct_video)
	{
		int mode = cfg.menu_pal ? 2 : 0;
		if (cfg.forced_scandoubler) mode++;

		memset(&v_def, 0, sizeof(v_def));

		v_def.item[0] = mode;
		for (int i = 0; i < 8; i++) v_def.item[i + 1] = tvmodes[mode].vpar[i];
		setPLL(tvmodes[mode].Fpix, &v_def);

		vmode_def = 1;
		vmode_pal = 0;
		vmode_ntsc = 0;
	}
	else
	{
		vmode_def = 0;
		if (!strlen(cfg.video_conf) && !strlen(cfg.video_conf_pal) && !strlen(cfg.video_conf_ntsc))
		{
			vmode_def = get_edid_vmode(&v_def);
		}

		if (!vmode_def)
		{
			vmode_def = store_custom_video_mode(cfg.video_conf, &v_def);
			vmode_pal = store_custom_video_mode(cfg.video_conf_pal, &v_pal);
			vmode_ntsc = store_custom_video_mode(cfg.video_conf_ntsc, &v_ntsc);
		}
	}
}

static void video_cfg_init()
{
	sprintf(gamma_cfg_path, "%s_gamma.cfg", user_io_get_core_name());
	sprintf(scaler_cfg_path, "%s_scaler.cfg", user_io_get_core_name());
	sprintf(shadow_mask_cfg_path, "%s_shmask.cfg", user_io_get_core_name());

	memset(gamma_cfg, 0, sizeof(gamma_cfg));
	memset(scaler_flt, 0, sizeof(scaler_flt));
	memset(shadow_mask_cfg, 0, sizeof(shadow_mask_cfg));

	if (cfg.preset_default[0])
	{
		char preset_path[1024];
		int len = sprintfz(preset_path, "%s/%s", PRESET_DIR, cfg.preset_default);
		if (len < 4 || strcasecmp(&preset_path[len - 4], ".ini"))
			strcat(preset_path, ".ini");
		video_loadPreset(preset_path, false);
	}

	loadGammaCfg();
	loadScalerCfg();
	loadShadowMaskCfg();
}

void video_cfg_reset()
{
	FileDeleteConfig(gamma_cfg_path);
	FileDeleteConfig(scaler_cfg_path);
	FileDeleteConfig(shadow_mask_cfg_path);

	video_cfg_init();

	setGamma();
	setScaler();
	setShadowMask();
}

void video_init()
{
	yc_parse(yc_modes, sizeof(yc_modes) / sizeof(yc_modes[0]));

	fb_init();
	hdmi_config_init();
	hdmi_config_set_hdr();
	video_mode_load();

	has_gamma = spi_uio_cmd(UIO_SET_GAMMA);

	video_cfg_init();

	video_set_mode(&v_def, 0);
}


static int api1_5 = 0;
int hasAPI1_5()
{
	return api1_5 || is_menu();
}

static bool get_video_info(bool force, VideoInfo *video_info)
{
	static uint16_t nres = 0;
	static bool vi_seen = false;
	bool res_changed = false;
	bool fb_changed = false;

	spi_uio_cmd_cont(UIO_GET_VRES);
	uint16_t res = spi_w(0);
	if ((nres != res) || force)
	{

		res_changed = (nres != res);
		nres = res;
		if (res_changed) vi_seen = true;
		video_info->width = spi_w(0) | (spi_w(0) << 16);
		video_info->height = spi_w(0) | (spi_w(0) << 16);
		video_info->htime = spi_w(0) | (spi_w(0) << 16);
		video_info->vtime = spi_w(0) | (spi_w(0) << 16);
		video_info->ptime = spi_w(0) | (spi_w(0) << 16);
		video_info->vtimeh = spi_w(0) | (spi_w(0) << 16);
		video_info->ctime = spi_w(0) | (spi_w(0) << 16);
		video_info->pixrep = spi_w(0);
		video_info->de_h = spi_w(0);
		video_info->de_v = spi_w(0);
		video_info->interlaced = ( res & 0x100 ) != 0;
		video_info->rotated = ( res & 0x200 ) != 0;
	}
	else
	{
		*video_info = current_video_info;
	}
	DisableIO();

	static uint8_t fb_crc = 0;
	uint8_t crc = spi_uio_cmd_cont(UIO_GET_FB_PAR);
	if (fb_crc != crc || force || res_changed)
	{
		fb_changed |= (fb_crc != crc);
		fb_crc = crc;
		video_info->arx = spi_w(0);
		video_info->arxy = !!(video_info->arx & 0x1000);
		video_info->arx &= 0xFFF;
		video_info->ary = spi_w(0) & 0xFFF;
		video_info->fb_fmt = spi_w(0);
		video_info->fb_width = spi_w(0);
		video_info->fb_height = spi_w(0);
		video_info->fb_en = !!(video_info->fb_fmt & 0x40);
	}
	DisableIO();

	return vi_seen && (res_changed || fb_changed);
}

static void video_core_description(const VideoInfo *vi, const vmode_custom_t */*vm*/, char *str, size_t len)
{
	float vrate = 100000000;
	if (vi->vtime) vrate /= vi->vtime; else vrate = 0;
	float hrate = 100000;
	if (vi->htime) hrate /= vi->htime; else hrate = 0;

	float prate = vi->width * 100;
	prate /= vi->ptime;

	char res[16];
	snprintf(res, 16, "%dx%d%s", vi->fb_en ? vi->fb_width : vi->width, vi->fb_en ? vi->fb_height : vi->height, vi->interlaced ? "i" : "");
	snprintf(str, len, "%9s %6.2fKHz %5.1fHz", res, hrate, vrate);
}

static void video_scaler_description(const VideoInfo *vi, const vmode_custom_t *vm, char *str, size_t len)
{
	char res[16];
	float vrateh = 100000000;
	if (vi->vtimeh) vrateh /= vi->vtimeh; else vrateh = 0;
	snprintf(res, 16, "%dx%d", vm->item[1] * (vm->param.pr ? 2 : 1), vm->item[5]);
	snprintf(str, len, "%9s %6.2fMHz %5.1fHz", res, vm->Fpix, vrateh);
}

void video_core_description(char *str, size_t len)
{
	video_core_description(&current_video_info, &v_cur, str, len);
}

void video_scaler_description(char *str, size_t len)
{
	video_scaler_description(&current_video_info, &v_cur, str, len);
}

char* video_get_core_mode_name(int with_vrefresh)
{
	static char tmp[256] = {};

	if (with_vrefresh)
	{
		float vrate = 100000000;
		if (current_video_info.vtime) vrate /= current_video_info.vtime; else vrate = 0;

		snprintf(tmp, sizeof(tmp), "%dx%d@%.1f", current_video_info.width, current_video_info.height, vrate);
	}
	else
	{
		snprintf(tmp, sizeof(tmp), "%dx%d", current_video_info.width, current_video_info.height);
	}

	return tmp;
}

static void show_video_info(const VideoInfo *vi, const vmode_custom_t *vm)
{
	float vrate = 100000000;
	if (vi->vtime) vrate /= vi->vtime; else vrate = 0;
	float hrate = 100000;
	if (vi->htime) hrate /= vi->htime; else hrate = 0;

	float prate = vi->width * 100;
	prate /= vi->ptime;

	float crate = vi->ctime * 100;
	crate /= vi->ptime;

	printf("\033[1;33mINFO: Video resolution: %u x %u%s, fHorz = %.1fKHz, fVert = %.1fHz, fPix = %.2fMHz, fVid = %.2fMHz\033[0m\n",
		vi->width, vi->height, vi->interlaced ? "i" : "", hrate, vrate, prate, crate);
	printf("\033[1;33mINFO: pr = %d, de_h = %d, de_v = %d\033[0m\n", vi->pixrep, vi->de_h, vi->de_v);
	printf("\033[1;33mINFO: Frame time (100MHz counter): VGA = %d, HDMI = %d\033[0m\n", vi->vtime, vi->vtimeh);
	printf("\033[1;33mINFO: AR = %d:%d, fb_en = %d, fb_width = %d, fb_height = %d\033[0m\n", vi->arx, vi->ary, vi->fb_en, vi->fb_width, vi->fb_height);
	if (vi->vtimeh) api1_5 = 1;
	if (hasAPI1_5() && cfg.video_info)
	{
		char str[128], res1[64], res2[64];
		video_core_description(vi, vm, res1, 64);
		video_scaler_description(vi, vm, res2, 64);
		snprintf(str, 128, "%s\n" \
			"\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\n" \
			"%s", res1, res2);
		Info(str, cfg.video_info * 1000);
	}
}

static void video_resolution_adjust(const VideoInfo *vi, vmode_custom_t *vm)
{
	if (cfg.vscale_mode < 4) return;

	int w = vm->param.pr ? vm->param.hact * 2 : vm->param.hact;
	int h = vm->param.vact;
	const uint32_t core_height = vi->fb_en ? vi->fb_height : vi->rotated ? vi->width : vi->height;
	const uint32_t core_width = vi->fb_en ? vi->fb_width : vi->rotated ? vi->height : vi->width;

	if (w == 0 || h == 0 || core_height == 0 || core_width == 0)
	{
		printf("video_resolution_adjust: invalid core or display sizes. Not adjusting resolution.\n");
		return;
	}

	int scale_h = h / core_height;
	if (!scale_h)
	{
		printf("video_resolution_adjust: display height less than core height. Not adjusting resolution.\n");
		return;
	}

	int ary = vi->ary;
	int arx = vi->arx;
	if (!ary || !arx)
	{
		ary = h;
		arx = w;
	}

	int scale_w = (w * ary) / (core_height * arx);
	if (!scale_w)
	{
		printf("video_resolution_adjust: display width less than core width. Not adjusting resolution.\n");
		return;
	}

	int scale = scale_h > scale_w ? scale_w : scale_h;

	int disp_h = core_height * scale;
	int core_ar_width = (disp_h * arx) / ary;
	int disp_ar_width = (disp_h * w) / h;
	int disp_w;

	if (cfg.vscale_mode == 5)
	{
		if (disp_ar_width < core_ar_width)
		{
			printf("video_resolution_adjust: ideal width %d wider than aspect restricted width %dx%d. Not adjusting resolution.\n", core_ar_width, disp_ar_width, disp_h);
			return;
		}
		disp_w = disp_ar_width;
		printf("video_resolution_adjust: using display aspect ratio - ");
	}
	else
	{
		disp_w = core_ar_width;
		printf("video_resolution_adjust: using core aspect ratio - ");
	}

	disp_w = (disp_w + 7) & ~0x7; // round up to 8

	printf("scale x%d, %dx%d.\n", scale, disp_w, disp_h);

	float refresh = 1000000.0 / ((vm->item[1] + vm->item[2] + vm->item[3] + vm->item[4])*(vm->item[5] + vm->item[6] + vm->item[7] + vm->item[8]) / vm->Fpix);
	video_calculate_cvt(disp_w, disp_h, refresh, vm->param.rb, vm);
	setPLL(vm->Fpix, vm);
}

static void video_scaling_adjust(const VideoInfo *vi, const vmode_custom_t *vm)
{
	if (cfg.vscale_mode >= 4)
	{
		spi_uio_cmd16(UIO_SETHEIGHT, 0);
		spi_uio_cmd16(UIO_SETWIDTH, 0);
		return;
	}

	const uint32_t height = vi->rotated ? vi->width : vi->height;

	uint32_t scrh = vm->item[5];
	if (scrh)
	{
		if (cfg.vscale_mode && height)
		{
			uint32_t div = 1 << (cfg.vscale_mode - 1);
			uint32_t mag = (scrh*div) / height;
			scrh = (height * mag) / div;
			printf("Set vertical scaling to : %d\n", scrh);
			spi_uio_cmd16(UIO_SETHEIGHT, scrh);
		}
		else if (cfg.vscale_border)
		{
			uint32_t border = cfg.vscale_border * 2;
			if ((border + 100) > scrh) border = scrh - 100;
			scrh -= border;
			printf("Set max vertical resolution to : %d\n", scrh);
			spi_uio_cmd16(UIO_SETHEIGHT, scrh);
		}
		else
		{
			spi_uio_cmd16(UIO_SETHEIGHT, 0);
		}
	}

	uint32_t scrw = vm->item[1];
	if (scrw)
	{
		if (cfg.vscale_border && !(cfg.vscale_mode && height))
		{
			uint32_t border = cfg.vscale_border * 2;
			if ((border + 100) > scrw) border = scrw - 100;
			scrw -= border;
			printf("Set max horizontal resolution to : %d\n", scrw);
			spi_uio_cmd16(UIO_SETWIDTH, scrw);
		}
		else
		{
			spi_uio_cmd16(UIO_SETWIDTH, 0);
		}
	}

	minimig_set_adjust(2);
}

bool video_mode_select(uint32_t vtime, vmode_custom_t* out_mode)
{
	vmode_custom_t *v = &v_def;
	bool adjustable = true;

	printf("\033[1;33mvideo_mode_select(%u): ", vtime);

	if (vtime == 0 || !cfg.vsync_adjust)
	{
		printf(", using default mode");
		adjustable = false;
	}
	else if (vmode_pal || vmode_ntsc)
	{
		if (vtime > 1800000)
		{
			if (vmode_pal)
			{
				printf(", using PAL mode");
				v = &v_pal;
			}
			else
			{
				printf(", PAL mode cannot be used. Using predefined NTSC mode");
				v = &v_ntsc;
				adjustable = false;
			}
		}
		else
		{
			if (vmode_ntsc)
			{
				printf(", using NTSC mode");
				v = &v_ntsc;
			}
			else
			{
				printf(", NTSC mode cannot be used. Using predefined PAL mode");
				v = &v_pal;
				adjustable = false;
			}
		}
	}
	else
	{
		printf(", using default mode");
	}
	printf(".\033[0m\n");
	memcpy(out_mode, v, sizeof(vmode_custom_t));
	return adjustable;
}

static void set_yc_mode()
{
	// Enable YC for S-Video/CVBS modes, or subcarrier for CXA2075 encoders
	if (cfg.vga_mode_int >= 2)
	{
		float fps = current_video_info.vtime ? (100000000.f / current_video_info.vtime) : 0.f;
		int pal = fps < 55.f;
		double CLK_REF = (pal || (cfg.ntsc_mode == 1)) ? 4.43361875f : (cfg.ntsc_mode == 2) ? 3.575611f : 3.579545f;
		double CLK_VIDEO = current_video_info.ctime * 100.f / current_video_info.ptime;

		float prate = current_video_info.width * 100.f;
		prate /= current_video_info.ptime;

		int64_t PHASE_INC = ((int64_t)((CLK_REF / CLK_VIDEO) * 1099511627776LL)) & 0xFFFFFFFFFFLL;

		int COLORBURST_START = (int)(3.7f * (CLK_VIDEO / CLK_REF));
		int COLORBURST_END = (int)(9.0f * (CLK_VIDEO / CLK_REF)) + COLORBURST_START;
		int COLORBURST_RANGE = (COLORBURST_START << 10) | COLORBURST_END;

		char yc_key[64];
		char yc_key_expand[64];
		sprintf(yc_key, "%s_%.1f%s%s", user_io_get_core_name(1), fps, current_video_info.interlaced ? "i" : "", (pal || !cfg.ntsc_mode) ? "" : (cfg.ntsc_mode == 1) ? "s" : "m");
		snprintf(yc_key_expand, sizeof(yc_key_expand), "%s_%.2f", yc_key, prate);
		printf("Calculated YC parameters for '%s': %s PHASE_INC=%lld, COLORBURST_START=%d, COLORBURST_END=%d\n", yc_key, pal ? "PAL" : (cfg.ntsc_mode == 1) ? "PAL60" : (cfg.ntsc_mode == 2) ? "PAL-M" : "NTSC", PHASE_INC, COLORBURST_START, COLORBURST_END);

		for (uint i = 0; i < sizeof(yc_modes) / sizeof(yc_modes[0]); i++)
		{
		if (!strcasecmp(yc_modes[i].key, yc_key) || !strcasecmp(yc_modes[i].key, yc_key_expand))
			{
				printf("Override YC PHASE_INC with value: %lld\n", yc_modes[i].phase_inc);
				PHASE_INC = yc_modes[i].phase_inc;
				break;
			}
		}

		spi_uio_cmd_cont(UIO_SET_YC_PAR);
		// For traditional S-Video/CVBS modes, enable YC processing
		// For subcarrier-only modes (RGB+subcarrier or direct video), keep yc_en=0
		bool is_subcarrier_only = (cfg.vga_mode_int == 4);
		uint16_t yc_config;
		if (is_subcarrier_only) {
			// Subcarrier-only: RGB mode with just PAL flag, yc_en=0
			yc_config = ((pal || cfg.ntsc_mode) ? 4 : 0);
		} else {
			// Traditional YC modes: enable YC processing
			yc_config = ((pal || cfg.ntsc_mode) ? 4 : 0) | ((cfg.vga_mode_int == 3) ? 3 : 1);
		}
		printf("Sending YC config to FPGA: 0x%02X (pal_en=%d, cvbs=%d, yc_en=%d)\n", yc_config, (yc_config >> 2) & 1, (yc_config >> 1) & 1, yc_config & 1);
		spi_w(yc_config);
		spi_w(PHASE_INC);
		spi_w(PHASE_INC >> 16);
		spi_w(PHASE_INC >> 32);
		spi_w(COLORBURST_RANGE);
		spi_w(COLORBURST_RANGE >> 16);
		// Case 6: Send subcarrier enable flag
		uint16_t subcarrier_enable = (cfg.vga_mode_int == 4) ? 1 : 0;
		printf("Sending subcarrier enable to FPGA: %d\n", subcarrier_enable);
		spi_w(subcarrier_enable);
		DisableIO();
	}
	else
	{
		spi_uio_cmd8(UIO_SET_YC_PAR, 0);
	}
}

static void spd_config_update()
{
	if (use_freesync_spd) return;

	if (cfg.direct_video)
	{
		// Custom SPD IF for additional DV1 metadata
		VideoInfo *vi = &current_video_info;
		if (!vi->width) return;

		uint8_t data[31] = {
			0x83, 0x01, 25, 0,
			'D',
			'V',
			'1', // version
			(uint8_t)((vi->interlaced ? 1 : 0) | (menu_present() ? 4 : 0) | (vi->rotated ? 8 : 0) | (arcade_get_direction() << 4)),
			(uint8_t)(vi->pixrep ? vi->pixrep : (vi->ctime / vi->width)),
			(uint8_t)vi->de_h,
			(uint8_t)(vi->de_h >> 8),
			(uint8_t)vi->de_v,
			(uint8_t)(vi->de_v >> 8),
			(uint8_t)vi->width,
			(uint8_t)(vi->width >> 8),
			(uint8_t)vi->height,
			(uint8_t)(vi->height >> 8)
		};

		char *name = user_io_get_core_name2();
		for (int i = 17; i < 31; i++)
		{
			if (!*name) break;
			data[i] = (uint8_t)(*name);
			name++;
		}

		hdmi_spd_config(data);
	}
	else
	{
		// Standard SPD IF
		uint8_t data[31] = {
			0x83, 0x01, 25,                        // SPD IF header + length
			0,                                    // Checksum, automatically calculated by ADV7513 if zero
			'M', 'i', 'S', 'T', 'e', 'r', 0, 0, // Vendor Name (up to 8 characters)
		};

		// Product Description (up to 16 characters)
		char *name = user_io_get_core_name2();
		for (int i = 12; i < 28; i++)
		{
			if (!*name) break;
			data[i] = (uint8_t)(*name);
			name++;
		}

		// Source Information (see ANSI/CTA-861-I, Table 35 - Source Product Description InfoFrame Data Byte 25)
		data[28] = 0x08; // Type: Game

		hdmi_spd_config(data);
	}
}

void video_mode_adjust()
{
	static bool force = false;

	VideoInfo video_info;

	const bool vid_changed = get_video_info(force, &video_info);

	if (vid_changed || force)
	{
		current_video_info = video_info;
		show_video_info(&video_info, &v_cur);
		set_yc_mode();
		spd_config_update();
	}
	force = false;

	static int menu = 0;
	int menu_now = menu_present();
	if(menu != menu_now) spd_config_update();
	menu = menu_now;

	if (vid_changed && !is_menu())
	{
		if (cfg_has_video_sections())
		{
			cfg_parse();
			video_mode_load();
			user_io_send_buttons(1);
		}

		if ((cfg.vsync_adjust || cfg.vscale_mode >= 4))
		{
			const uint32_t vtime = video_info.vtime;

			printf("\033[1;33madjust_video_mode(%u): vsync_adjust=%d vscale_mode=%d.\033[0m\n", vtime, cfg.vsync_adjust, cfg.vscale_mode);

			vmode_custom_t new_mode;
			bool adjust = video_mode_select(vtime, &new_mode);

			video_resolution_adjust(&video_info, &new_mode);

			vmode_custom_t *v = &new_mode;
			double Fpix = 0;
			if (adjust)
			{
				Fpix = 100 * (v->item[1] + v->item[2] + v->item[3] + v->item[4]) * (v->item[5] + v->item[6] + v->item[7] + v->item[8]);
				Fpix /= vtime;
				if (Fpix < 2.f || Fpix > 300.f)
				{
					printf("Estimated Fpix(%.4f MHz) is outside supported range. Canceling auto-adjust.\n", Fpix);
					Fpix = 0;
				}

				float hz = 100000000.0f / vtime;
				if (cfg.refresh_min && hz < cfg.refresh_min)
				{
					printf("Estimated frame rate (%f Hz) is less than REFRESH_MIN(%f Hz). Canceling auto-adjust.\n", hz, cfg.refresh_min);
					Fpix = 0;
				}

				if (cfg.refresh_max && hz > cfg.refresh_max)
				{
					printf("Estimated frame rate (%f Hz) is more than REFRESH_MAX(%f Hz). Canceling auto-adjust.\n", hz, cfg.refresh_max);
					Fpix = 0;
				}
			}

			video_set_mode(v, Fpix);
			user_io_send_buttons(1);
			force = true;
		}
		else if (cfg_has_video_sections()) // if we have video sections but aren't updating the resolution for other reasons, then do it here
		{
			video_set_mode(&v_def, 0);
			user_io_send_buttons(1);
			force = true;
		}
		else
		{
			set_vfilter(1); // force update filters in case interlacing changed
		}

		video_scaling_adjust(&video_info, &v_cur);
	}
	else
	{
		set_vfilter(0); // update filters if flags have changed
	}
}

static void fb_write_module_params()
{
	int width = fb_width;
	int height = fb_height;
	offload_add_work([=]
	{
		FILE *fp = fopen("/sys/module/MiSTer_fb/parameters/mode", "wt");
		if (fp)
		{
			fprintf(fp, "%d %d %d %d %d\n", 8888, 1, width, height, width * 4);
			fclose(fp);
		}
	});
}

void video_fb_enable(int enable, int n)
{
	PROFILE_FUNCTION();

	if (fb_base)
	{
		int res = spi_uio_cmd_cont(UIO_SET_FBUF);
		if (res)
		{
			if (is_menu() && !enable && menu_bg)
			{
				enable = 1;
				n = menu_bgn;
			}

			if (enable)
			{
				uint32_t fb_addr = FB_ADDR + (FB_SIZE * 4 * n) + (n ? 0 : 4096);
				fb_num = n;

				int xoff = 0, yoff = 0;
				if (cfg.direct_video)
				{
					xoff = v_cur.item[4] - FB_DV_LBRD;
					yoff = v_cur.item[8] - FB_DV_UBRD;
				}

				//printf("Switch to Linux frame buffer\n");
				spi_w((uint16_t)(FB_EN | FB_FMT_RxB | FB_FMT_8888)); // format, enable flag
				spi_w((uint16_t)fb_addr); // base address low word
				spi_w(fb_addr >> 16);     // base address high word
				spi_w(fb_width);          // frame width
				spi_w(fb_height);         // frame height
				spi_w(xoff);                 // scaled left
				spi_w(xoff + v_cur.item[1] - 1); // scaled right
				spi_w(yoff);                 // scaled top
				spi_w(yoff + v_cur.item[5] - 1); // scaled bottom
				spi_w(fb_width * 4);      // stride

				//printf("Linux frame buffer: %dx%d, stride = %d bytes\n", fb_width, fb_height, fb_width * 4);
				if (!fb_num)
				{
					fb_write_module_params();
					input_switch(0);
				}
				else
				{
					input_switch(1);
				}
			}
			else
			{
				printf("Switch to core frame buffer\n");
				spi_w(0); // enable flag
				input_switch(1);
			}

			fb_enabled = enable;
		}
		else
		{
			printf("Core doesn't support HPS frame buffer\n");
			input_switch(1);
		}

		DisableIO();
		if (cfg.direct_video) set_vga_fb(enable);
		if (is_menu()) user_io_status_set("[8:5]", (fb_enabled && !fb_num) ? 0x160 : 0);
	}
}

int video_fb_state()
{
	if (is_menu())
	{
		return fb_enabled && !fb_num;
	}

	return fb_enabled;
}


static void video_fb_config()
{
	PROFILE_FUNCTION();

	int fb_scale = cfg.fb_size;

	if (fb_scale <= 1)
	{
		if (((v_cur.item[1] * v_cur.item[5]) > FB_SIZE))
			fb_scale = 2;
		else
			fb_scale = 1;
	}
	else if (fb_scale == 3) fb_scale = 2;
	else if (fb_scale > 4) fb_scale = 4;

	const int fb_scale_x = fb_scale;
	const int fb_scale_y = v_cur.param.pr == 0 ? fb_scale : fb_scale * 2;

	fb_width = v_cur.item[1] / fb_scale_x;
	fb_height = v_cur.item[5] / fb_scale_y;

	brd_x = cfg.vscale_border / fb_scale_x;
	brd_y = cfg.vscale_border / fb_scale_y;

	if (fb_enabled) video_fb_enable(1, fb_num);

	fb_write_module_params();
}

static void draw_checkers()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);

	uint32_t col1 = 0x888888;
	uint32_t col2 = 0x666666;
	int sz = fb_width / 128;

	for (int y = brd_y; y < fb_height - brd_y; y++)
	{
		int c1 = (y / sz) & 1;
		int pos = y * fb_width;
		for (int x = brd_x; x < fb_width - brd_x; x++)
		{
			int c2 = c1 ^ ((x / sz) & 1);
			buf[pos + x] = c2 ? col2 : col1;
		}
	}
}

static void draw_hbars1()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);
	int height = fb_height - 2 * brd_y;

	int old_base = 0;
	int gray = 255;
	int sz = height / 7;
	int stp = 0;

	for (int y = brd_y; y < fb_height - brd_y; y++)
	{
		int pos = y * fb_width;
		int base_color = ((7 * (y-brd_y)) / height) + 1;
		if (old_base != base_color)
		{
			stp = sz;
			old_base = base_color;
		}

		gray = 255 * stp / sz;

		for (int x = brd_x; x < fb_width - brd_x; x++)
		{
			uint32_t color = 0;
			if (base_color & 4) color |= gray;
			if (base_color & 2) color |= gray << 8;
			if (base_color & 1) color |= gray << 16;
			buf[pos + x] = color;
		}

		stp--;
		if (stp < 0) stp = 0;
	}
}

static void draw_hbars2()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);
	int height = fb_height - 2 * brd_y;
	int width = fb_width - 2 * brd_x;

	for (int y = brd_y; y < fb_height - brd_y; y++)
	{
		int pos = y * fb_width;
		int base_color = ((14 * (y - brd_y)) / height);
		int inv = base_color & 1;
		base_color >>= 1;
		base_color = (inv ? base_color : 6 - base_color) + 1;
		for (int x = brd_x; x < fb_width - brd_x; x++)
		{
			int gray = (256 * (x - brd_x)) / width;
			if (inv) gray = 255 - gray;
			uint32_t color = 0;
			if (base_color & 4) color |= gray;
			if (base_color & 2) color |= gray << 8;
			if (base_color & 1) color |= gray << 16;
			buf[pos + x] = color;
		}
	}
}

static void draw_vbars1()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);
	int width = fb_width - 2 * brd_x;

	int sz = width / 7;
	int stp = 0;

	for (int y = brd_y; y < fb_height - brd_y; y++)
	{
		int pos = y * fb_width;
		int old_base = 0;
		int gray = 255;
		for (int x = brd_x; x < fb_width - brd_x; x++)
		{
			int base_color = ((7 * (x - brd_x)) / width) + 1;
			if (old_base != base_color)
			{
				stp = sz;
				old_base = base_color;
			}

			gray = 255 * stp / sz;

			uint32_t color = 0;
			if (base_color & 4) color |= gray;
			if (base_color & 2) color |= gray << 8;
			if (base_color & 1) color |= gray << 16;
			buf[pos + x] = color;

			stp--;
			if (stp < 0) stp = 0;
		}
	}
}

static void draw_vbars2()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);
	int height = fb_height - 2 * brd_y;
	int width = fb_width - 2 * brd_x;

	for (int y = brd_y; y < fb_height - brd_y; y++)
	{
		int pos = y * fb_width;
		for (int x = brd_x; x < fb_width - brd_x; x++)
		{
			int gray = ((256 * (y - brd_y)) / height);
			int base_color = ((14 * (x - brd_x)) / width);
			int inv = base_color & 1;
			base_color >>= 1;
			base_color = (inv ? base_color : 6 - base_color) + 1;

			if (inv) gray = 255 - gray;
			uint32_t color = 0;
			if (base_color & 4) color |= gray;
			if (base_color & 2) color |= gray << 8;
			if (base_color & 1) color |= gray << 16;
			buf[pos + x] = color;
		}
	}
}

static void draw_spectrum()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);
	int height = fb_height - 2 * brd_y;
	int width = fb_width - 2 * brd_x;

	for (int y = brd_y; y < fb_height - brd_y; y++)
	{
		int pos = y * fb_width;
		int blue = ((256 * (y - brd_y)) / height);
		for (int x = brd_x; x < fb_width - brd_x; x++)
		{
			int green = ((256 * (x - brd_x)) / width) - blue / 2;
			int red = 255 - green - blue / 2;
			if (red < 0) red = 0;
			if (green < 0) green = 0;

			buf[pos + x] = (red << 16) | (green << 8) | blue;
		}
	}
}

static void draw_black()
{
	volatile uint32_t* buf = fb_base + (FB_SIZE*menu_bgn);

	for (int y = 0; y < fb_height; y++)
	{
		int pos = y * fb_width;
		for (int x = 0; x < fb_width; x++) buf[pos++] = 0;
	}
}

static uint64_t getus()
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return (tv.tv_sec * 10000000) + tv.tv_usec;
}

static void vs_wait()
{
	int fb = open("/dev/fb0", O_RDWR | O_CLOEXEC);
	int zero = 0;
	uint64_t t1, t2;
	if (ioctl(fb, FBIO_WAITFORVSYNC, &zero) == -1)
	{
		printf("fb ioctl failed: %s\n", strerror(errno));
		close(fb);
		return;
	}

	t1 = getus();
	ioctl(fb, FBIO_WAITFORVSYNC, &zero);
	t2 = getus();
	close(fb);

	printf("vs_wait(us): %llu\n", t2 - t1);
}

static char *get_file_fromdir(const char* dir, int num, int *count)
{
	static char name[256+32];
	name[0] = 0;
	if(count) *count = 0;
	DIR *d = opendir(getFullPath(dir));
	if (d)
	{
		int cnt = 0;
		struct dirent *de = readdir(d);
		while (de)
		{
			int len = strlen(de->d_name);
			if (len > 4 && (de->d_name[0] != '.') && (!strcasecmp(de->d_name + len - 4, ".png") || !strcasecmp(de->d_name + len - 4, ".jpg")))
			{
				if (num == cnt) break;
				cnt++;
			}

			de = readdir(d);
		}

		if (de)
		{
			snprintf(name, sizeof(name), "%s/%s", dir, de->d_name);
		}
		closedir(d);
		if(count) *count = cnt;
	}

	return name;
}

static Imlib_Image load_bg()
{
	const char* fname = "menu.png";
	if (!FileExists(fname))
	{
		fname = "menu.jpg";
		if (!FileExists(fname)) fname = 0;
	}

	if (!fname)
	{
		static char bgdir[128];
		static char label[64];
		int alt = altcfg();

		const char* cfg_name = cfg_get_name(alt);
		snprintf(label, sizeof(label), "%s", cfg_name + 6);
		char *p = strrchr(label, '.');
		if (p) *p = 0;

		sprintf(bgdir, "wallpapers%s", label);
		if (alt <= 0 || !cfg_name[0] || !PathIsDir(bgdir)) strcpy(bgdir, "wallpapers");

		if (PathIsDir(bgdir))
		{
			int rndfd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
			if (rndfd >= 0)
			{
				uint32_t rnd;
				read(rndfd, &rnd, sizeof(rnd));
				close(rndfd);

				int count = 0;
				get_file_fromdir(bgdir, -1, &count);
				if (count > 0) fname = get_file_fromdir(bgdir, rnd % count, &count);
			}
		}
	}

	if (fname)
	{
		Imlib_Load_Error error = IMLIB_LOAD_ERROR_NONE;
		Imlib_Image img = imlib_load_image_with_error_return(getFullPath(fname), &error);
		if (img) return img;
		printf("Image %s loading error %d\n", fname, error);
	}

	return NULL;
}

static int bg_has_picture = 0;
extern uint8_t  _binary_logo_png_start[], _binary_logo_png_end[];
void video_menu_bg(int n, int idle)
{
	bg_has_picture = 0;
	menu_bg = n;
	if (n)
	{
		//printf("**** BG DEBUG START ****\n");
		//printf("n = %d\n", n);

		Imlib_Load_Error error;
		static Imlib_Image logo = 0;
		if (!logo)
		{
			unlink("/tmp/logo.png");
			if (FileSave("/tmp/logo.png", _binary_logo_png_start, _binary_logo_png_end - _binary_logo_png_start))
			{
				while(1)
				{
					error = IMLIB_LOAD_ERROR_NONE;
					if ((logo = imlib_load_image_with_error_return("/tmp/logo.png", &error))) break;
					else
					{
						if (error != IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT)
						{
							printf("logo.png error = %d\n", error);
							break;
						}
					}
					vs_wait();
				};

				if (cfg.osd_rotate)
				{
					imlib_context_set_image(logo);
					imlib_image_orientate(cfg.osd_rotate == 1 ? 3 : 1);
				}
			}
			else
			{
				printf("Fail to save to /tmp/logo.png\n");
			}
			unlink("/tmp/logo.png");
			printf("Logo = %p\n", logo);
		}

		menu_bgn = (menu_bgn == 1) ? 2 : 1;

		static Imlib_Image menubg = 0;
		static Imlib_Image bg1 = 0, bg2 = 0;
		if (!bg1) bg1 = imlib_create_image_using_data(fb_width, fb_height, (uint32_t*)(fb_base + (FB_SIZE * 1)));
		if (!bg1) printf("Warning: bg1 is 0\n");
		if (!bg2) bg2 = imlib_create_image_using_data(fb_width, fb_height, (uint32_t*)(fb_base + (FB_SIZE * 2)));
		if (!bg2) printf("Warning: bg2 is 0\n");

		Imlib_Image *bg = (menu_bgn == 1) ? &bg1 : &bg2;
		//printf("*bg = %p\n", *bg);

		static Imlib_Image curtain = 0;
		if (!curtain)
		{
			curtain = imlib_create_image(fb_width, fb_height);
			imlib_context_set_image(curtain);
			imlib_image_set_has_alpha(1);

			uint32_t *data = imlib_image_get_data();
			int sz = fb_width * fb_height;
			for (int i = 0; i < sz; i++)
			{
				*data++ = 0x9F000000;
			}
		}

		draw_black();

		if (idle < 3)
		{
			switch (n)
			{
			case 1:
				if (!menubg) menubg = load_bg();
				if (menubg)
				{
					imlib_context_set_image(menubg);
					int src_w = imlib_image_get_width();
					int src_h = imlib_image_get_height();
					//printf("menubg: src_w=%d, src_h=%d\n", src_w, src_h);

					if (*bg)
					{
						imlib_context_set_image(*bg);
						imlib_blend_image_onto_image(menubg, 0,
							0, 0,                           //int source_x, int source_y,
							src_w, src_h,                   //int source_width, int source_height,
							brd_x, brd_y,                   //int destination_x, int destination_y,
							fb_width - (brd_x * 2), fb_height - (brd_y * 2) //int destination_width, int destination_height
						);
						bg_has_picture = 1;
						break;
					}
					else
					{
						printf("*bg = 0!\n");
					}
				}
				draw_checkers();
				break;
			case 2:
				draw_hbars1();
				break;
			case 3:
				draw_hbars2();
				break;
			case 4:
				draw_vbars1();
				break;
			case 5:
				draw_vbars2();
				break;
			case 6:
				draw_spectrum();
				break;
			case 7:
				draw_black();
				break;
			}
		}

		if (cfg.logo && logo && !idle)
		{
			imlib_context_set_image(logo);

			int src_w = imlib_image_get_width();
			int src_h = imlib_image_get_height();

			printf("logo: src_w=%d, src_h=%d\n", src_w, src_h);

			int width = fb_width - (brd_x * 2);
			int height = fb_height - (brd_y * 2);

			int dst_w, dst_h;
			int dst_x, dst_y;
			if (cfg.osd_rotate)
			{
				dst_h = height / 2;
				dst_w = src_w * dst_h / src_h;
				if (cfg.osd_rotate == 1)
				{
					dst_x = brd_x;
					dst_y = height - dst_h;
				}
				else
				{
					dst_x = width - dst_w;
					dst_y = brd_y;
				}
			}
			else
			{
				dst_x = brd_x;
				dst_y = brd_y;
				dst_w = width * 2 / 7;
				dst_h = src_h * dst_w / src_w;
			}

			if (*bg)
			{
				if (cfg.direct_video && (v_cur.item[5] < 300)) dst_h /= 2;

				imlib_context_set_image(*bg);
				imlib_blend_image_onto_image(logo, 1,
					0, 0,         //int source_x, int source_y,
					src_w, src_h, //int source_width, int source_height,
					dst_x, dst_y, //int destination_x, int destination_y,
					dst_w, dst_h  //int destination_width, int destination_height
				);
			}
			else
			{
				printf("*bg = 0!\n");
			}
		}

		if (curtain)
		{
			if (idle > 1 && *bg)
			{
				imlib_context_set_image(*bg);
				imlib_blend_image_onto_image(curtain, 1,
					0, 0,                //int source_x, int source_y,
					fb_width, fb_height, //int source_width, int source_height,
					0, 0,                //int destination_x, int destination_y,
					fb_width, fb_height  //int destination_width, int destination_height
				);
			}
		}
		else
		{
			printf("curtain = 0!\n");
		}

		//test the fb driver
		//vs_wait();
		//printf("**** BG DEBUG END ****\n");
	}

	video_fb_enable(0);
}

int video_bg_has_picture()
{
	return bg_has_picture;
}

int video_chvt(int num)
{
	static int cur_vt = 0;
	if (num)
	{
		cur_vt = num;
		int fd;
		if ((fd = open("/dev/tty0", O_RDONLY | O_CLOEXEC)) >= 0)
		{
			if (ioctl(fd, VT_ACTIVATE, cur_vt)) printf("ioctl VT_ACTIVATE fails\n");
			if (ioctl(fd, VT_WAITACTIVE, cur_vt)) printf("ioctl VT_WAITACTIVE fails\n");
			close(fd);
		}
	}

	return cur_vt ? cur_vt : 1;
}

void video_cmd(char *cmd)
{
	if (video_fb_state())
	{
		int accept = 0;
		int fmt = 0, rb = 0, div = -1, width = -1, height = -1;
		uint16_t hmin, hmax, vmin, vmax;
		if (sscanf(cmd, "fb_cmd0 %d %d %d", &fmt, &rb, &div) == 3)
		{
			if (div >= 1 && div <= 4)
			{
				width = v_cur.item[1] / div;
				height = v_cur.item[5] / div;
				hmin = vmin = 0;
				hmax = v_cur.item[1] - 1;
				vmax = v_cur.item[5] - 1;
				accept = 1;
			}
		}

		if (sscanf(cmd, "fb_cmd2 %d %d %d", &fmt, &rb, &div) == 3)
		{
			if (div >= 1 && div <= 4)
			{
				width = v_cur.item[1] / div;
				height = v_cur.item[5] / div;
				hmin = vmin = 0;
				hmax = v_cur.item[1] - 1;
				vmax = v_cur.item[5] - 1;
				accept = 1;
			}
		}

		if (sscanf(cmd, "fb_cmd1 %d %d %d %d", &fmt, &rb, &width, &height) == 4)
		{
			if (width < 120 || width > (int)v_cur.item[1]) width = v_cur.item[1];
			if (height < 120 || height > (int)v_cur.item[5]) height = v_cur.item[5];

			int divx = 1;
			int divy = 1;
			if (cfg.direct_video && (v_cur.item[5] < 300))
			{
				// TV 240P/288P
				while ((width*(divx + 1)) <= (int)v_cur.item[1]) divx++;
				while ((height*(divy + 1)) <= (int)v_cur.item[5]) divy++;
			}
			else
			{
				while ((width*(divx + 1)) <= (int)v_cur.item[1] && (height*(divx + 1)) <= (int)v_cur.item[5]) divx++;
				divy = divx;
			}

			hmin = (uint16_t)((v_cur.item[1] - (width * divx)) / 2);
			vmin = (uint16_t)((v_cur.item[5] - (height * divy)) / 2);
			hmax = hmin + (width * divx) - 1;
			vmax = vmin + (height * divy) - 1;
			accept = 1;
		}

		int bpp = 0;
		int sc_fmt = 0;

		if (accept)
		{
			switch (fmt)
			{
			case 8888:
				bpp = 4;
				sc_fmt = FB_FMT_8888;
				break;

			case 1555:
				bpp = 2;
				sc_fmt = FB_FMT_1555;
				break;

			case 565:
				bpp = 2;
				sc_fmt = FB_FMT_565;
				break;

			case 8:
				bpp = 1;
				sc_fmt = FB_FMT_PAL8;
				rb = 0;
				break;

			default:
				accept = 0;
			}
		}

		if (rb)
		{
			sc_fmt |= FB_FMT_RxB;
			rb = 1;
		}

		if(accept)
		{
			int stride = ((width * bpp) + 15) & ~15;
			printf("fb_cmd: new mode: %dx%d => %dx%d color=%d stride=%d\n", width, height, hmax - hmin + 1, vmax - vmin + 1, fmt, stride);

			uint32_t addr = FB_ADDR + 4096;

			int xoff = 0, yoff = 0;
			if (cfg.direct_video)
			{
				xoff = v_cur.item[4] - FB_DV_LBRD;
				yoff = v_cur.item[8] - FB_DV_UBRD;
			}

			spi_uio_cmd_cont(UIO_SET_FBUF);
			spi_w(FB_EN | sc_fmt); // format, enable flag
			spi_w((uint16_t)addr); // base address low word
			spi_w(addr >> 16);     // base address high word
			spi_w(width);          // frame width
			spi_w(height);         // frame height
			spi_w(xoff + hmin);    // scaled left
			spi_w(xoff + hmax);    // scaled right
			spi_w(yoff + vmin);    // scaled top
			spi_w(yoff + vmax);    // scaled bottom
			spi_w(stride);         // stride
			DisableIO();

			if (cmd[6] != '2')
			{
				static char cmd[256];
				sprintf(cmd, "echo %d %d %d %d %d >/sys/module/MiSTer_fb/parameters/mode", fmt, rb, width, height, stride);
				system(cmd);
			}
		}
		else
		{
			printf("video_cmd: unknown command or format.\n");
		}
	}
}

void video_mode_cmd(char *cmd)
{
	vmode_custom_t v = {};
	int ret = parse_custom_video_mode(cmd, &v);
	if (ret != -2)
	{
		printf("video_mode_cmd: only custom modelines are supported, got \"%s\"\n", cmd);
		return;
	}

	v_def = v;
	v_cur = v;
	video_set_mode(&v, v.Fpix);
	user_io_send_buttons(1);
	printf("video_mode_cmd: applied mode \"%s\"\n", cmd);
}

static constexpr int CELL_GRAN_RND = 4;

static int determine_vsync(int w, int h)
{
    const int arx[] =   {4, 16, 16, 5, 15};
    const int ary[] =   {3,  9, 10, 4, 9 };
	const int vsync[] = {4,  5,  6, 7, 7 };

    for (int ar = 0; ar < 5; ar++)
    {
        int w_calc = ((h * arx[ar]) / (ary[ar] * CELL_GRAN_RND)) * CELL_GRAN_RND;
        if (w_calc == w)
        {
            return vsync[ar];
        }
    }

    return 10;
}

static void video_calculate_cvt_int(int h_pixels, int v_lines, float refresh_rate, bool reduced_blanking, vmode_custom_t *vmode)
{
	// Based on xfree86 cvt.c and https://tomverbeure.github.io/video_timings_calculator

	const float CLOCK_STEP = 0.25f;
	const int MIN_V_BPORCH = 6;
	const int V_FRONT_PORCH = 3;

	const int h_pixels_rnd = (h_pixels / CELL_GRAN_RND) * CELL_GRAN_RND;
	const int v_sync = determine_vsync(h_pixels_rnd, v_lines);

	int v_back_porch;
	int h_blank, h_sync, h_back_porch, h_front_porch;
	int total_pixels;
	float pixel_freq;

	if (reduced_blanking)
	{
		const int RB_V_FPORCH = 3;
		const float RB_MIN_V_BLANK = 460.0f;

		float h_period_est = ((1000000.0f / refresh_rate) - RB_MIN_V_BLANK) / (float)v_lines;
		h_blank = 160;

		int vbi_lines = (int)(RB_MIN_V_BLANK / h_period_est) + 1;

		int rb_min_vbi = RB_V_FPORCH + v_sync + MIN_V_BPORCH;
		int act_vbi_lines = (vbi_lines < rb_min_vbi) ? rb_min_vbi : vbi_lines;

		int total_v_lines = act_vbi_lines + v_lines;

		total_pixels = h_blank + h_pixels_rnd;

		pixel_freq = CLOCK_STEP * floorf((refresh_rate * (float)(total_v_lines * total_pixels) / 1000000.0f) / CLOCK_STEP);

		v_back_porch = act_vbi_lines - V_FRONT_PORCH - v_sync;

		h_sync = 32;
		h_back_porch = 80;
		h_front_porch = h_blank - h_sync - h_back_porch;
	}
	else
	{
		const float MIN_VSYNC_BP = 550.0f;
		const float C_PRIME = 30.0f;
		const float M_PRIME = 300.0f;
		const float H_SYNC_PER = 0.08f;

		const float h_period_est = ((1.0f / refresh_rate) - MIN_VSYNC_BP / 1000000.0f) / (float)(v_lines + V_FRONT_PORCH) * 1000000.0f;

		int v_sync_bp = (int)(MIN_VSYNC_BP / h_period_est) + 1;
		if (v_sync_bp < (v_sync + MIN_V_BPORCH))
		{
			v_sync_bp = v_sync + MIN_V_BPORCH;
		}

		v_back_porch = v_sync_bp - v_sync;

		float ideal_duty_cycle = C_PRIME - (M_PRIME * h_period_est / 1000.0f);

		if (ideal_duty_cycle < 20)
		{
			h_blank = (h_pixels_rnd / 4 / (2 * CELL_GRAN_RND)) * (2 * CELL_GRAN_RND);
		}
		else
		{
			h_blank = (int)((float)h_pixels_rnd * ideal_duty_cycle / (100.0f - ideal_duty_cycle) / (2 * CELL_GRAN_RND)) * (2 * CELL_GRAN_RND);
		}

		total_pixels = h_pixels_rnd + h_blank;

		h_sync = (int)(H_SYNC_PER * (float)total_pixels / CELL_GRAN_RND) * CELL_GRAN_RND;
		h_back_porch = h_blank / 2;
		h_front_porch = h_blank - h_sync - h_back_porch;

		pixel_freq = CLOCK_STEP * floorf((float)total_pixels / h_period_est / CLOCK_STEP);
	}

	vmode->item[0] = 1;
	vmode->param.hact = h_pixels_rnd;
	vmode->param.hfp = h_front_porch;
	vmode->param.hs = h_sync;
	vmode->param.hbp = h_back_porch;
	vmode->param.vact = v_lines;
	vmode->param.vfp = V_FRONT_PORCH - 1;
	vmode->param.vs = v_sync;
	vmode->param.vbp = v_back_porch + 1;
	vmode->param.rb = reduced_blanking ? 1 : 0;
	vmode->Fpix = pixel_freq;

	if (h_pixels_rnd > 2048)
	{
		vmode->param.pr = 1;
		vmode->param.hact /= 2;
		vmode->param.hbp /= 2;
		vmode->param.hfp /= 2;
		vmode->param.hs /= 2;
		vmode->Fpix /= 2.0;
	}
	else
	{
		vmode->param.pr = 0;
	}


	printf("Calculated %dx%d@%0.1fhz %s timings: %d,%d,%d,%d,%d,%d,%d,%d,%d,%s%s\n",
		h_pixels, v_lines, refresh_rate, reduced_blanking ? "CVT-RB" : "CVT",
		vmode->item[1], vmode->item[2], vmode->item[3], vmode->item[4],
		vmode->item[5], vmode->item[6], vmode->item[7], vmode->item[8],
		(int)(pixel_freq * 1000.0f),
		reduced_blanking ? "cvtrb" : "cvt",
		vmode->param.pr ? ",pr" : "");
}

static void video_calculate_cvt(int h_pixels, int v_lines, float refresh_rate, int reduced_blanking, vmode_custom_t *vmode)
{
	// If the resolution it too wide and the core doesn't support pixel repetition then just do 1080p
	if (h_pixels > 2048 && !supports_pr())
	{
		printf("Pixel repetition not supported by core for %dx%d resolution, defaulting 1080p.\n", h_pixels, v_lines);
		video_calculate_cvt(1920, 1080, refresh_rate, reduced_blanking, vmode);
		return;
	}

	video_calculate_cvt_int(h_pixels, v_lines, refresh_rate, reduced_blanking == 1, vmode);
	if (vmode->Fpix > 210.f && reduced_blanking == 2)
	{
		printf("Calculated pixel clock is too high. Trying CVT-RB timings.\n");
		video_calculate_cvt_int(h_pixels, v_lines, refresh_rate, 1, vmode);
	}
}



int video_get_rotated()
{
  return current_video_info.rotated;
}


