// xBRZ 3x Pixel Art Scaler
// High-quality pixel art scaling algorithm with edge detection

//!BGFX EFFECT
//!VERSION 1
//!NAME xBRZ 3x
//!CATEGORY Pixel Art
//!DESCRIPTION Scales pixel art by 3x using the xBRZ algorithm with edge-aware interpolation


//!TEXTURE
Texture2D INPUT;

//!TEXTURE
//!WIDTH INPUT_WIDTH * 3
//!HEIGHT INPUT_HEIGHT * 3
Texture2D OUTPUT;

//!SAMPLER
//!FILTER POINT
SamplerState sam;


//!PASS 1
//!IN INPUT
//!OUT OUTPUT
//!BLOCK_SIZE 24
//!NUM_THREADS 64

// Blend mode constants
#define BLEND_NONE 0
#define BLEND_NORMAL 1
#define BLEND_DOMINANT 2

// Algorithm tuning parameters
#define LUMINANCE_WEIGHT 1.0
#define EQUAL_COLOR_TOLERANCE 30.0/255.0
#define STEEP_DIRECTION_THRESHOLD 2.2
#define DOMINANT_DIRECTION_THRESHOLD 3.6

static const float one_third = 1.0 / 3.0;
static const float two_third = 2.0 / 3.0;

// Converts color to a scalar for comparison
float reduce(const float3 color) {
	return dot(color, float3(65536.0, 256.0, 1.0));
}

// Calculates perceptual color distance using YCbCr color space
float DistYCbCr(const float3 pixA, const float3 pixB) {
	const float3 weights = float3(0.2627, 0.6780, 0.0593);
	const float scaleB = 0.5 / (1.0 - weights.b);
	const float scaleR = 0.5 / (1.0 - weights.r);
	float3 diff = pixA - pixB;
	float Y = dot(diff, weights);
	float Cb = scaleB * (diff.b - Y);
	float Cr = scaleR * (diff.r - Y);

	return sqrt(((LUMINANCE_WEIGHT * Y) * (LUMINANCE_WEIGHT * Y)) + (Cb * Cb) + (Cr * Cr));
}

// Tests if two pixels are considered equal within tolerance
bool IsPixEqual(const float3 pixA, const float3 pixB) {
	return (DistYCbCr(pixA, pixB) < EQUAL_COLOR_TOLERANCE);
}

// Checks if any blending is required for the pixel
bool IsBlendingNeeded(const int4 blend) {
	return any(!(blend == int4(BLEND_NONE, BLEND_NONE, BLEND_NONE, BLEND_NONE)));
}

// Performs pixel scaling with edge-aware blending for 3x scale
void ScalePixel(const int4 blend, const float3 k[9], inout float3 dst[9]) {
	float v0 = reduce(k[0]);
	float v4 = reduce(k[4]);
	float v5 = reduce(k[5]);
	float v7 = reduce(k[7]);
	float v8 = reduce(k[8]);

	float dist_01_04 = DistYCbCr(k[1], k[4]);
	float dist_03_08 = DistYCbCr(k[3], k[8]);
	bool haveShallowLine = (STEEP_DIRECTION_THRESHOLD * dist_01_04 <= dist_03_08) && (v0 != v4) && (v5 != v4);
	bool haveSteepLine = (STEEP_DIRECTION_THRESHOLD * dist_03_08 <= dist_01_04) && (v0 != v8) && (v7 != v8);
	bool needBlend = (blend[2] != BLEND_NONE);
	bool doLineBlend = (blend[2] >= BLEND_DOMINANT ||
		!((blend[1] != BLEND_NONE && !IsPixEqual(k[0], k[4])) ||
			(blend[3] != BLEND_NONE && !IsPixEqual(k[0], k[8])) ||
			(IsPixEqual(k[4], k[3]) && IsPixEqual(k[3], k[2]) && IsPixEqual(k[2], k[1]) && IsPixEqual(k[1], k[8]) && !IsPixEqual(k[0], k[2]))));

	float3 blendPix = (DistYCbCr(k[0], k[1]) <= DistYCbCr(k[0], k[3])) ? k[1] : k[3];
	dst[1] = lerp(dst[1], blendPix, (needBlend && doLineBlend) ? ((haveSteepLine) ? 0.750 : ((haveShallowLine) ? 0.250 : 0.125)) : 0.000);
	dst[2] = lerp(dst[2], blendPix, (needBlend) ? ((doLineBlend) ? ((!haveShallowLine && !haveSteepLine) ? 0.875 : 1.000) : 0.4545939598) : 0.000);
	dst[3] = lerp(dst[3], blendPix, (needBlend && doLineBlend) ? ((haveShallowLine) ? 0.750 : ((haveSteepLine) ? 0.250 : 0.125)) : 0.000);
	dst[4] = lerp(dst[4], blendPix, (needBlend && doLineBlend && haveShallowLine) ? 0.250 : 0.000);
	dst[8] = lerp(dst[8], blendPix, (needBlend && doLineBlend && haveSteepLine) ? 0.250 : 0.000);
}

// Destination index mapping for 3x3 output grid
const static uint destIdx[3][3] = {
	{6, 7, 8},
	{5, 0, 1},
	{4, 3, 2}
};

//---------------------------------------
// Input Pixel Mapping:  --|21|22|23|--
//                       19|06|07|08|09
//                       18|05|00|01|10
//                       17|04|03|02|11
//                       --|15|14|13|--
//
// Output Pixel Mapping:    06|07|08
//                          05|00|01
//                          04|03|02
void Pass1(uint2 blockStart, uint3 threadId) {
	const uint2 gxy = (TileSwizzle8x8(threadId.x) * 3) + blockStart;

	const uint2 outputSize = GetOutputSize();
	if (gxy.x >= outputSize.x || gxy.y >= outputSize.y) {
		return;
	}

	const float2 inputPt = GetInputPt();
	const float2 pos = ((gxy / 3) + 0.5f) * inputPt;

	//    A1 B1 C1
	// A0 A  B  C  C4
	// D0 D  E  F  F4
	// G0 G  H  I  I4
	//    G5 H5 I5

	const float4 t1 = pos.xxxy + float4(-inputPt.x, 0, inputPt.x, -2.0 * inputPt.y);
	const float4 t2 = pos.xxxy + float4(-inputPt.x, 0, inputPt.x, -inputPt.y);
	const float4 t3 = pos.xxxy + float4(-inputPt.x, 0, inputPt.x, 0);
	const float4 t4 = pos.xxxy + float4(-inputPt.x, 0, inputPt.x, inputPt.y);
	const float4 t5 = pos.xxxy + float4(-inputPt.x, 0, inputPt.x, 2.0 * inputPt.y);
	const float4 t6 = pos.xyyy + float4(-2.0 * inputPt.x, -inputPt.y, 0, inputPt.y);
	const float4 t7 = pos.xyyy + float4(2.0 * inputPt.x, -inputPt.y, 0, inputPt.y);

	//---------------------------------------
	// Input Pixel Mapping:  20|21|22|23|24
	//                       19|06|07|08|09
	//                       18|05|00|01|10
	//                       17|04|03|02|11
	//                       16|15|14|13|12

	float3 src[25];

	src[21] = INPUT.SampleLevel(sam, t1.xw, 0).rgb;
	src[22] = INPUT.SampleLevel(sam, t1.yw, 0).rgb;
	src[23] = INPUT.SampleLevel(sam, t1.zw, 0).rgb;
	src[6] = INPUT.SampleLevel(sam, t2.xw, 0).rgb;
	src[7] = INPUT.SampleLevel(sam, t2.yw, 0).rgb;
	src[8] = INPUT.SampleLevel(sam, t2.zw, 0).rgb;
	src[5] = INPUT.SampleLevel(sam, t3.xw, 0).rgb;
	src[0] = INPUT.SampleLevel(sam, t3.yw, 0).rgb;
	src[1] = INPUT.SampleLevel(sam, t3.zw, 0).rgb;
	src[4] = INPUT.SampleLevel(sam, t4.xw, 0).rgb;
	src[3] = INPUT.SampleLevel(sam, t4.yw, 0).rgb;
	src[2] = INPUT.SampleLevel(sam, t4.zw, 0).rgb;
	src[15] = INPUT.SampleLevel(sam, t5.xw, 0).rgb;
	src[14] = INPUT.SampleLevel(sam, t5.yw, 0).rgb;
	src[13] = INPUT.SampleLevel(sam, t5.zw, 0).rgb;
	src[19] = INPUT.SampleLevel(sam, t6.xy, 0).rgb;
	src[18] = INPUT.SampleLevel(sam, t6.xz, 0).rgb;
	src[17] = INPUT.SampleLevel(sam, t6.xw, 0).rgb;
	src[9] = INPUT.SampleLevel(sam, t7.xy, 0).rgb;
	src[10] = INPUT.SampleLevel(sam, t7.xz, 0).rgb;
	src[11] = INPUT.SampleLevel(sam, t7.xw, 0).rgb;

	float v[9];
	v[0] = reduce(src[0]);
	v[1] = reduce(src[1]);
	v[2] = reduce(src[2]);
	v[3] = reduce(src[3]);
	v[4] = reduce(src[4]);
	v[5] = reduce(src[5]);
	v[6] = reduce(src[6]);
	v[7] = reduce(src[7]);
	v[8] = reduce(src[8]);

	int4 blendResult = int4(BLEND_NONE, BLEND_NONE, BLEND_NONE, BLEND_NONE);

	// Process corners for blend detection
	// Pixel Tap Mapping: --|--|--|--|--
	//                    --|--|07|08|--
	//                    --|05|00|01|10
	//                    --|04|03|02|11
	//                    --|--|14|13|--

	// Corner (1, 1)
	if (!((v[0] == v[1] && v[3] == v[2]) || (v[0] == v[3] && v[1] == v[2]))) {
		float dist_03_01 = DistYCbCr(src[4], src[0]) + DistYCbCr(src[0], src[8]) + DistYCbCr(src[14], src[2]) + DistYCbCr(src[2], src[10]) + (4.0 * DistYCbCr(src[3], src[1]));
		float dist_00_02 = DistYCbCr(src[5], src[3]) + DistYCbCr(src[3], src[13]) + DistYCbCr(src[7], src[1]) + DistYCbCr(src[1], src[11]) + (4.0 * DistYCbCr(src[0], src[2]));
		bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_03_01) < dist_00_02;
		blendResult[2] = ((dist_03_01 < dist_00_02) && (v[0] != v[1]) && (v[0] != v[3])) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
	}


	// Pixel Tap Mapping: --|--|--|--|--
	//                    --|06|07|--|--
	//                    18|05|00|01|--
	//                    17|04|03|02|--
	//                    --|15|14|--|--
	// Corner (0, 1)
	if (!((v[5] == v[0] && v[4] == v[3]) || (v[5] == v[4] && v[0] == v[3]))) {
		float dist_04_00 = DistYCbCr(src[17], src[5]) + DistYCbCr(src[5], src[7]) + DistYCbCr(src[15], src[3]) + DistYCbCr(src[3], src[1]) + (4.0 * DistYCbCr(src[4], src[0]));
		float dist_05_03 = DistYCbCr(src[18], src[4]) + DistYCbCr(src[4], src[14]) + DistYCbCr(src[6], src[0]) + DistYCbCr(src[0], src[2]) + (4.0 * DistYCbCr(src[5], src[3]));
		bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_05_03) < dist_04_00;
		blendResult[3] = ((dist_04_00 > dist_05_03) && (v[0] != v[5]) && (v[0] != v[3])) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
	}

	// Pixel Tap Mapping: --|--|22|23|--
	//                    --|06|07|08|09
	//                    --|05|00|01|10
	//                    --|--|03|02|--
	//                    --|--|--|--|--
	// Corner (1, 0)
	if (!((v[7] == v[8] && v[0] == v[1]) || (v[7] == v[0] && v[8] == v[1]))) {
		float dist_00_08 = DistYCbCr(src[5], src[7]) + DistYCbCr(src[7], src[23]) + DistYCbCr(src[3], src[1]) + DistYCbCr(src[1], src[9]) + (4.0 * DistYCbCr(src[0], src[8]));
		float dist_07_01 = DistYCbCr(src[6], src[0]) + DistYCbCr(src[0], src[2]) + DistYCbCr(src[22], src[8]) + DistYCbCr(src[8], src[10]) + (4.0 * DistYCbCr(src[7], src[1]));
		bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_07_01) < dist_00_08;
		blendResult[1] = ((dist_00_08 > dist_07_01) && (v[0] != v[7]) && (v[0] != v[1])) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
	}

	// Pixel Tap Mapping: --|21|22|--|--
	//                    19|06|07|08|--
	//                    18|05|00|01|--
	//                    --|04|03|--|--
	//                    --|--|--|--|--
	// Corner (0, 0)
	if (!((v[6] == v[7] && v[5] == v[0]) || (v[6] == v[5] && v[7] == v[0]))) {
		float dist_05_07 = DistYCbCr(src[18], src[6]) + DistYCbCr(src[6], src[22]) + DistYCbCr(src[4], src[0]) + DistYCbCr(src[0], src[8]) + (4.0 * DistYCbCr(src[5], src[7]));
		float dist_06_00 = DistYCbCr(src[19], src[5]) + DistYCbCr(src[5], src[3]) + DistYCbCr(src[21], src[7]) + DistYCbCr(src[7], src[1]) + (4.0 * DistYCbCr(src[6], src[0]));
		bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_05_07) < dist_06_00;
		blendResult[0] = ((dist_05_07 < dist_06_00) && (v[0] != v[5]) && (v[0] != v[7])) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
	}

	float3 dst[9];
	dst[0] = src[0];
	dst[1] = src[0];
	dst[2] = src[0];
	dst[3] = src[0];
	dst[4] = src[0];
	dst[5] = src[0];
	dst[6] = src[0];
	dst[7] = src[0];
	dst[8] = src[0];

	// Apply pixel scaling with blending
	if (IsBlendingNeeded(blendResult)) {
		float3 k[9];
		float3 tempDst8;
		float3 tempDst7;

		k[8] = src[8];
		k[7] = src[7];
		k[6] = src[6];
		k[5] = src[5];
		k[4] = src[4];
		k[3] = src[3];
		k[2] = src[2];
		k[1] = src[1];
		k[0] = src[0];
		ScalePixel(blendResult.xyzw, k, dst);

		k[8] = src[6];
		k[7] = src[5];
		k[6] = src[4];
		k[5] = src[3];
		k[4] = src[2];
		k[3] = src[1];
		k[2] = src[8];
		k[1] = src[7];
		tempDst8 = dst[8];
		tempDst7 = dst[7];
		dst[8] = dst[6];
		dst[7] = dst[5];
		dst[6] = dst[4];
		dst[5] = dst[3];
		dst[4] = dst[2];
		dst[3] = dst[1];
		dst[2] = tempDst8;
		dst[1] = tempDst7;
		ScalePixel(blendResult.wxyz, k, dst);

		k[8] = src[4];
		k[7] = src[3];
		k[6] = src[2];
		k[5] = src[1];
		k[4] = src[8];
		k[3] = src[7];
		k[2] = src[6];
		k[1] = src[5];
		tempDst8 = dst[8];
		tempDst7 = dst[7];
		dst[8] = dst[6];
		dst[7] = dst[5];
		dst[6] = dst[4];
		dst[5] = dst[3];
		dst[4] = dst[2];
		dst[3] = dst[1];
		dst[2] = tempDst8;
		dst[1] = tempDst7;
		ScalePixel(blendResult.zwxy, k, dst);

		k[8] = src[2];
		k[7] = src[1];
		k[6] = src[8];
		k[5] = src[7];
		k[4] = src[6];
		k[3] = src[5];
		k[2] = src[4];
		k[1] = src[3];
		tempDst8 = dst[8];
		tempDst7 = dst[7];
		dst[8] = dst[6];
		dst[7] = dst[5];
		dst[6] = dst[4];
		dst[5] = dst[3];
		dst[4] = dst[2];
		dst[3] = dst[1];
		dst[2] = tempDst8;
		dst[1] = tempDst7;
		ScalePixel(blendResult.yzwx, k, dst);

		// Rotate destination pixels back to original orientation
		tempDst8 = dst[8];
		tempDst7 = dst[7];
		dst[8] = dst[6];
		dst[7] = dst[5];
		dst[6] = dst[4];
		dst[5] = dst[3];
		dst[4] = dst[2];
		dst[3] = dst[1];
		dst[2] = tempDst8;
		dst[1] = tempDst7;
	}

	[unroll]
	for (uint i = 0; i < 3; ++i) {
		[unroll]
		for (uint j = 0; j < 3; ++j) {
			OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], 1);
		}
	}
}
