The Scanline Classic Shader

UPDATE 2024-04-20: Version 5.0 now available

This is official support thread for my Scanline Classic shader. You can find the latest version with instructions here: https://github.com/anikom15/scanline-classic

Right now only slang is supported, but I plan to port a GLSL version for standalone emulators and a BGFX version for MAME. There is also only one preset. I plan to make presets for a variety of systems and use cases.

Features:

  • Color correction and transformations: the shader supports three color mode. The first two are monochrome modes. You can specify either one or two color primaries which determine the tone of grayscale. This can be used to simulate a variety of monochrome monitors. The third is the color mode. Any three-primary color space can be simulated by providing the appropriate chromaticity values and weights. If you need help determining the values for a particular look, feel free to ask for help.

  • Wide gamut support: particularly useful for monochrome modes, the colors are calculated according to the gamut supported by your display. sRGB, DCI, Adobe RGB, and BT.2020 are supported.

  • Phosphor decay simulation: the shader simulates phosphor decay in XYZ space, which has the advantage of allowing some very cool effects when used with the two-primary monochrome mode. Other shaders do their decay in RGB space, so these effects wouldn’t be possible.

  • Scanline and interlacing simulation: Flexible scanlines can be added. The scanlines are always solid, but the ratio of active scanline width to blank scanline width can be adjusted. Line-doubling can also be simulated by setting ON_PIXELS to 2 and OFF_PIXELS to 0. Interlacing can be supported by setting an appropriate HI_RES_THRES value and this allows for automatic progressive and interlaced mode switching for systems like the PlayStation.

  • Mult-scan support: The shader can cope with systems that have variable resolutions, like Macintosh and Windows 98.

  • Geometry simulation: There are settings for pincushioning and barreling the image. Various tweaks are possible to get a variety of effects. These effects account for the display aspect ratio, something many shaders don’t account for. The overall output can be scaled to a desired picture fill.

  • Shadow mask simulation: Several of the usual shadow mask patterns are available.

Here are some live images showing support for different color spaces:

Here are screenshots showing what this shader can do:

7 Likes

Updates on what I’ve been working on

I want a shader that can mimic the functionality of Multi-Scan CRTs. It’s a myth that CRTs have no native resolution or can handle any display mode thrown at them, otherwise we would have never needed circuitry to handle NTSC signals on PAL televisions! While CRTs are a lot more flexible than LCDs in how they present a signal, they are in many ways a lot less flexibile and scaleable than LCDs because they usually only operate on a fixed horizontal frequency. For decades that was about 15 kHz, but that started to change in the 16-bit computing era.

One of the pioneers for faster scanning displays was Atari, who used ‘medium res’ monitors in some of their games. The idea behind medium res is simple, take the resolution of a PAL monitor and combine it with the refresh rate of an NTSC monitor. Voila, you have a monitor that can display more lines without the flicker.

If you see a computer system or arcade game that runs at around 384 lines, you know it’s a medium res game. Mac 68ks and the IBM EGA monitor used these resolutions.

Multitasking started to become more important in the mid-80s. 240 and 384 line displays were adequate for single-task workflows, but felt cramped in multi-tasking environments. The easiest way to experience this is to emulate something like a Mac SE/30 and exploring the file system with Finder. Your screen will fill up with windows very quickly. 480 line displays were sorely needed. The Japanese had already solved this problem with 480i modes (because their language requires more pixel-space to render clearly), but this was improved upon by doubling the 15 kHz scan rate to 31 kHz, and thus 480p monitors came about.

But what if we want to display a 15 kHz signal on a 31 kHz monitor? Now the issue of being able to drive multiple signal types with a CRT really needed to be solved. It couldn’t simply be ignored because we needed backwards compatibility with old software designed for ~240 line environment. Video cards also supported more colors on the lower resolutions, which were critical for games. The Japanese (again) had already solved this problem. When a 31 kHz monitor encounters a 15 kHz signal, it doubles the lines and displays the signal with the vertical resolution essentially upscaled as NN. But these monitors were quite expensive. IBM came up with the solution of doing the line-doubling with the video card, so any 31 kHz monitor can be used with the ‘15 kHz’ signals.

Finally it was in the 90s where Multi-Scan capable monitors started to become more common. A CRT monitor would be advertised as supporting a maximum resolution, and the CRT would have built-in circuitry to handle most of the common modes. The CRT could theoretically handle a much larger variety of modes as long as it fell within the configurable horizontal frequency range. Over time, the minimum frequency increased until it capped to 31 kHz, VGA frequency, because VGA compatibility was so critical.

Why does any of this matter for shaders? It doesn’t matter at all for consoles (at least not until we get into HD consoles, at which point CRTs were so rare it’s perhaps not meaningful to have CRT shaders for them). It only matters for computer emulation, and only if we care about the artifacts of a multi-scan monitor. There are three primary complications that multi-scanning has: 1. Scanlines become more visible as lower resolutions are used; 2. the overall brightness of the screen diminishes at lower resolutions (because of 1); and 3. the geometry and positioning of the screen changes depending on the resolution chosen.

1 and 2 are easy enough to implement. We just have to decide on what the ideal resolution of our simulated monitor is. That resolution would not have visible scanlines. When the emulator switches to a mode with a smaller resolution, we would add scanlines. Adding the scanlines would automatically cover 2. 3 is complicated, and every monitor would handle these differently. Actually, it goes back to number 1, because they wouldn’t necessarily cover the same picture area with a lower resolution, and so scanlines might not scale linearly in that respect. Another complication is aspect ratio. Most monitors would just spit everything out in whatever aspect ratio they were designed for, so a 4:3 monitor would distort a 5:4 resolution and vice versa. Late professional monitors (very expensive) could actually handle different aspect ratios and display them correctly.

But 3 is really a non-issue when you consider that by the time Multi-Scan monitors were common, geometry controls on monitors were also common, and usually the user would adjust the picture to fill out the screen to a comfortable (usually we would leave a bit of space to avoid too much curvature). So really it is just 1 that I’m going to focus on.

5 Likes

FYI the Multi-Scan feature has been added and the shader is now at version 4.1. At this point I feel like the shader has all the features needed, but I’ll play with it for a few months to see if I can think of anything else to add.

As a bonus here’s a screenshot of DOS in 80x25 mode on a simulated 1024x768 monitor:

7 Likes

Hey there anikom15,

I read your comment about contrast simulation of a CRT in this other thread (https://forums.libretro.com/t/old-monitor-style-shader-and-overlay-for-dosbox-pure/41535/26 ). That’s definitely an interesting topic. Since you seem to value gamma quality I thought you may find the following about contrast simulation useful.

For simulating the analog CRT “brightness” (black level in analog terms) and “contrast” (brightness in analog terms) of old monitors and TV’s implementing BT. 1886 gamma control seems to do exactly what you want regarding analog contrast, without faking it. Note that Appendix 1 and 2 of the official spec document is what you would want to look into, not the vanilla implementation.

I’ve written a bit about it in the past in another thread and separately from this recently Dogway has implemented it in his grade - update 2023.slang shader.

My post on it here: SpectraCal’s Director of Software Development, Joel Barsotti, argues that the BT. 1886 gamma function (EOTF) more accurately matches legacy CRT technology than previous power law based functions. The argument is that it properly takes into account the legacy “contrast” and “brightness” controls on CRTs.

SpectraCal FAQ on BT. 1886 here: BT.1886 – 10 Questions, 10 Answers

The relevant Rec. ITU-R BT. 1886 document here: Recommendation ITU-R BT.1886 . Be sure to read Appendix 1 and Appendix 2 as that is what you would want to play around with.

Not that you need it, but if you want a quick look at the effect of implementing these analog controls through BT. 1886 EOTF you could try Dogway’s GRADE shader. Here’s his recent implementation forum post: I updated Grade with what I’ve learned in the last 2 years. And his GitHub with his latest RC6 release of the Grade shader: grade - update 2023 RC6.slang

4 Likes

FYI, a bunch of monochrome presets are now available in the repo.

@rafan, thanks for pointing this out. I’m not sure if I want to add this complexity at this point, but I think it will end up being something that gets added to the advanced shader later down the road.

3 Likes

Well, porting this to MAME is on halt because I can’t seem to compile bgfx-tools and I haven’t been able to get in touch with the guy who maintains that. I probably didn’t set up my environment correctly.

I think I will add another parameter which will break compatibility again. I think having a MIN_SCAN_RATE parameter to finely control the line doubler is justified.

An update! But nothing new to try, just plans for the future. Life moves on and unfortunately I do have a fulltime job and lifestyle I have to keep up. But I digress.

After almost a year of using this shader, I am very happy with it. I never got it ported to MAME’s BGFX driver, but I did create a single pass version that can work as a standalone GLSL shader. I’ll provide that as an update sometime soon. This can be used in many emulators that Retroarch doesn’t support, like 86Box.

The big new thing I’m going to work on this year though will be support for colorspaces other than sRGB! My TV supports 10- and 12-bit input and I just got a video card that can support it. My TV also can accept input signals for DCI, Adobe RGB, and BT.2020 (we’ll see if I can find out how much coverage of each color space the TV supports). Here’s a quick overview of what all of that means:

Adobe RGB is the oldest and was originally used by Photoshop to provide a bigger colorspace for photographers to edit their work in. Adobe RGB is mainly supported in expensive professional monitors. It has largely been supplanted by other spaces.

DCI is the color space used by digital cinema equipment. It is famously used by Apple with some slight variations in their iPhone (and maybe some Mac OS X support?). This could be an important space to support for Retroarch on mobile.

BT.2020 is the largest and most recent of the color spaces. It’s really meant to cover as much of our visible color spectrum as possible in order to display content made in either sRGB, DCI, or something else. This will be the one I work on first as this is the current standard for UHD and HDR content.

10- and 12-bit color is also important. sRGB uses 8-bit color, so if you try to use a larger gamut with just 8 bits you’ll get banding. How visible that is is a matter of debate, but 10-bit and 12-bit color provide enough dynamic range to present any of the above color spaces without banding. My display and video card supports both modes and I will test both.

My TV also supports automatically switching colorspace depending on the content providing, but I’m not sure how that will work with Windows and Retroarch. For now, I will have to manually set the colorspace through the shader and through the TV to match. The big question is if Retroarch will support a 10-bit or 12-bit framebuffer. If it doesn’t, I’ll have to modify it to work. This might be tricky to test!

3 Likes

Version 5.0 is ready. This release includes support for other color spaces. You can see the differences in the screenshots below which were made by using an iPhone and a manual camera app. The white balance was fixed to 6500 K with an ISO of 250 and a shutter speed of 120. Obviously not ideal fidelity, but good enough to highlight the limitations of sRGB in the greens and reds. RTINGS.com states that my TV has a coverage of 88% DCI and 64% BT.2020, 100% sRGB and Adobe RGB was not measured. The difference is subtle, however. To my eye, DCI isn’t a good choice, but this may be due to limitations of my TV. DCI overall looks less saturated. BT.2020 and Adobe RGB look good.

It seems 8-bit color is good enough for the Super Nintendo. Banding may become apparent with something higher fidelity, but for now higher-bit color support will be deprioritized in favor of Scanline LCD (which is a misnomer, but I want to keep the brand consistent). Scanline LCD will be a comprehensive shader for LCD displays, including monochrome, reflective, front-lit, back-lit, etc.

More screenshots of the color space modes can be found here: https://postimg.cc/gallery/cN54H3s

5 Likes

Wow! Great photos! These look almost like GPU Screenshots!

I couldn’t find version 5 on Github.

Really nice shader. I searched a long time for a good one to use for PC and Commodore Emulation. For some reason the 86Box Emulator only uses glsl files. For that I have combined your scanline-basic.glsl shader with the curvature of Hyllian’s. Now it is perfect for me to use with 86Box and on my Rpi!

scanline-basic-curvature.glsl
/* Filename: scanline-basic-curvature.glsl

Red Queen: I modified scanline-basic.glsl shader to include Hyllian's screen curvature!

   Copyright (C) 2010 Team XBMC
   Copyright (C) 2011 Stefanos A.
   Copyright (C) 2023 W. M. Martinez
   Copyright (C) 2025 Red Queen

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>. */

const float EPS = 1.19209289551e-7;
const float PI = 3.14159265359;
const vec3 BLACK = vec3(0.0, 0.0, 0.0);
const vec3 WHITE = vec3(1.0, 1.0, 1.0);
const vec3 RED = vec3(1.0, 0.0, 0.0);
const vec3 YELLOW = vec3(1.0, 1.0, 0.0);
const vec3 GREEN = vec3(0.0, 1.0, 0.0);
const vec3 BLUE = vec3(0.0, 1.0, 1.0);
const vec3 INDIGO = vec3(0.0, 0.0, 1.0);
const vec3 VIOLET = vec3(1.0, 0.0, 1.0);

float sq(const float x)
{
	return x * x;
}

float cb(const float x)
{
	return x * x * x;
}

float crt_linear(const float x)
{
	return pow(x, 2.4);
}

vec3 crt_linear(const vec3 x)
{
	return vec3(crt_linear(x.r), crt_linear(x.g), crt_linear(x.b));
}

float crt_gamma(const float x)
{
	return pow(x, 1.0 / 2.4);
}

vec3 crt_gamma(const vec3 x)
{
	return vec3(crt_gamma(x.r), crt_gamma(x.g), crt_gamma(x.b));
}

float sdr_linear(const float x)
{
	return x < 0.081 ? x / 4.5 : pow((x + 0.099) / 1.099, 1.0 / 0.45);
}

vec3 sdr_linear(const vec3 x)
{
	return vec3(sdr_linear(x.r), sdr_linear(x.g), sdr_linear(x.b));
}

float sdr_gamma(const float x)
{
	return x < 0.018 ? 4.5 * x : 1.099 * pow(x, 0.45) - 0.099;
}

vec3 sdr_gamma(const vec3 x)
{
	return vec3(sdr_gamma(x.r), sdr_gamma(x.g), sdr_gamma(x.b));
}

float srgb_linear(const float x)
{
	return x <= 0.04045 ? x / 12.92 : pow((x + 0.055) / 1.055, 2.4);
}

vec3 srgb_linear(const vec3 x)
{
	return vec3(srgb_linear(x.r), srgb_linear(x.g), srgb_linear(x.b));
}

float srgb_gamma(const float x)
{
	return x <= 0.0031308 ? 12.92 * x : 1.055 * pow(x, 1.0 / 2.4) - 0.055;
}

vec3 srgb_gamma(const vec3 x)
{
	return vec3(srgb_gamma(x.r), srgb_gamma(x.g), srgb_gamma(x.b));
}

mat3 XYZ_TO_sRGB = mat3(
	 3.2406255, -0.9689307,  0.0557101,
	-1.5372080,  1.8758561, -0.2040211,
	-0.4986286,  0.0415175,  1.0569959);

mat3 colorspace_rgb()
{
	return XYZ_TO_sRGB;
}

vec3 RGB_to_xyY(const float x, const float y, const vec3 Y, const vec3 RGB)
{
	return vec3(x, y, dot(Y, RGB));
}

vec3 xyY_to_XYZ(const vec3 xyY)
{
	float x = xyY.x;
	float y = xyY.y;
	float Y = xyY.z;
	float z = 1.0 - x - y;

	return vec3(Y * x / y, Y, Y * z / y);
}

float kernel(const float x, const float B, const float C)
{
	float dx = abs(x);

	if (dx < 1.0) {
		float P1 = 2.0 - 3.0 / 2.0 * B - C;
		float P2 = -3.0 + 2.0 * B + C;
		float P3 = 1.0 - 1.0 / 3.0 * B;

		return P1 * cb(dx) + P2 * sq(dx) + P3;
	} else if ((dx >= 1.0) && (dx < 2.0)) {
		float P1 = -1.0 / 6.0 * B - C;
		float P2 = B + 5.0 * C;
		float P3 = -2.0 * B - 8.0 * C;
		float P4 = 4.0 / 3.0 * B + 4.0 * C;

		return P1 * cb(dx) + P2 * sq(dx) + P3 * dx + P4;
	} else
		return 0.0;
}

vec4 kernel4(const float x, const float B, const float C)
{
	return vec4(kernel(x - 2.0, B, C),
	            kernel(x - 1.0, B, C),
	            kernel(x, B, C),
	            kernel(x + 1.0, B, C));
}

#pragma parameter TRANSFER_FUNCTION "Transfer function" 1.0 1.0 2.0 1.0 
#pragma parameter COLOR_MODE "Chromaticity mode" 3.0 1.0 3.0 1.0
#pragma parameter LUMINANCE_WEIGHT_R "Red channel luminance weight" 0.2124 0.0 1.0 0.01
#pragma parameter LUMINANCE_WEIGHT_G "Green channel luminance weight" 0.7011 0.0 1.0 0.01
#pragma parameter LUMINANCE_WEIGHT_B "Blue channel luminance weight" 0.0866 0.0 1.0 0.01 
#pragma parameter CHROMA_A_X "Chromaticity A x" 0.630 0.0 1.0 0.001
#pragma parameter CHROMA_A_Y "Chromaticity A y" 0.340 0.0 1.0 0.001
#pragma parameter CHROMA_B_X "Chromaticity B x" 0.310 0.0 1.0 0.001
#pragma parameter CHROMA_B_Y "Chromaticity B y" 0.595 0.0 1.0 0.001
#pragma parameter CHROMA_C_X "Chromaticity C x" 0.155 0.0 1.0 0.001
#pragma parameter CHROMA_C_Y "Chromaticity C y" 0.070 0.0 1.0 0.001
#pragma parameter CHROMA_A_WEIGHT "Chromaticity A luminance weight" 0.2124 0.0 1.0 0.01
#pragma parameter CHROMA_B_WEIGHT "Chromaticity B luminance weight" 0.7011 0.0 1.0 0.01
#pragma parameter CHROMA_C_WEIGHT "Chromaticity C luminance weight" 0.0866 0.0 1.0 0.01
#pragma parameter SCALE_W "Scale white point" 1.0 0.0 1.0 1.0 
#pragma parameter SCAN_TYPE "Scan type" 1.0 1.0 2.0 1.0
#pragma parameter MAX_SCAN_RATE "Maximum active lines" 480.0 1.0 1200.0 1.0
#pragma parameter LINE_DOUBLER "Enable line-doubler" 0.0 0.0 1.0 1.0
#pragma parameter INTER_OFF "Interlace offset" 0.0 0.0 1.0 1.0
#pragma parameter FOCUS "Focus (%)" 0.50 0.0 1.0 0.01
#pragma parameter ZOOM "Viewport zoom" 1.0 0.0 10.0 0.01
#pragma parameter COLOR_SPACE "Output color space" 1.0 1.0 4.0 1.0
#pragma parameter CRT_CURVATURE "CRT-Curvature" 1.0 0.0 1.0 1.0
#pragma parameter CRT_warpX "CRT-Curvature X-Axis" 0.031 0.0 0.125 0.01
#pragma parameter CRT_warpY "CRT-Curvature Y-Axis" 0.041 0.0 0.125 0.01
#pragma parameter CRT_cornersize "CRT-Corner Size" 0.01 0.001 1.0 0.005
#define cornersize CRT_cornersize
#pragma parameter CRT_cornersmooth "CRT-Corner Smoothness" 1000.0 80.0 2000.0 100.0
#define cornersmooth CRT_cornersmooth

#if defined(VERTEX)

#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#endif

#ifdef GL_ES
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

COMPAT_ATTRIBUTE vec4 VertexCoord;
COMPAT_ATTRIBUTE vec4 COLOR;
COMPAT_ATTRIBUTE vec4 TexCoord;
COMPAT_VARYING vec4 COL0;
COMPAT_VARYING vec4 TEX0;

uniform mat4 MVPMatrix;
uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;

#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize)
#define OriginalSize vec4(InputSize, 1.0 / InputSize)
#define OutputSize vec4(OutputSize, 1.0 / OutputSize)

void main()
{
	gl_Position = MVPMatrix * VertexCoord;
	TEX0.xy = TexCoord.xy;
}

#elif defined(FRAGMENT)

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_TEXTURE texture
out COMPAT_PRECISION vec4 FragColor;
#else
#define COMPAT_VARYING varying
#define FragColor gl_FragColor
#define COMPAT_TEXTURE texture2D
#endif

uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;
uniform sampler2D Texture;
COMPAT_VARYING vec4 TEX0;

#define Source Texture
#define vTexCoord TEX0.xy

#define SourceSize vec4(TextureSize, 1.0 / TextureSize)
#define OriginalSize vec4(InputSize, 1.0 / InputSize)
#define OutputSize vec4(OutputSize, 1.0 / OutputSize)

#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float TRANSFER_FUNCTION;
uniform COMPAT_PRECISION float COLOR_MODE;
uniform COMPAT_PRECISION float LUMINANCE_WEIGHT_R;
uniform COMPAT_PRECISION float LUMINANCE_WEIGHT_G;
uniform COMPAT_PRECISION float LUMINANCE_WEIGHT_B;
uniform COMPAT_PRECISION float CHROMA_A_X;
uniform COMPAT_PRECISION float CHROMA_A_Y;
uniform COMPAT_PRECISION float CHROMA_B_X;
uniform COMPAT_PRECISION float CHROMA_B_Y;
uniform COMPAT_PRECISION float CHROMA_C_X;
uniform COMPAT_PRECISION float CHROMA_C_Y;
uniform COMPAT_PRECISION float CHROMA_A_WEIGHT;
uniform COMPAT_PRECISION float CHROMA_B_WEIGHT;
uniform COMPAT_PRECISION float CHROMA_C_WEIGHT;
uniform COMPAT_PRECISION float SCALE_W;
uniform COMPAT_PRECISION float SCAN_TYPE;
uniform COMPAT_PRECISION float MAX_SCAN_RATE;
uniform COMPAT_PRECISION float LINE_DOUBLER;
uniform COMPAT_PRECISION float INTER_OFF;
uniform COMPAT_PRECISION float FOCUS;
uniform COMPAT_PRECISION float ZOOM;
uniform COMPAT_PRECISION float COLOR_SPACE;
uniform COMPAT_PRECISION float CRT_CURVATURE;
uniform COMPAT_PRECISION float CRT_warpX;
uniform COMPAT_PRECISION float CRT_warpY;
uniform COMPAT_PRECISION float CRT_cornersize;
uniform COMPAT_PRECISION float CRT_cornersmooth;
#else
#define TRANSFER_FUNCTION 1.0
#define COLOR_MODE 3.0
#define LUMINANCE_WEIGHT_R 0.2124
#define LUMINANCE_WEIGHT_G 0.7011
#define LUMINANCE_WEIGHT_B 0.0866
#define CHROMA_A_X 0.630
#define CHROMA_A_Y 0.340
#define CHROMA_B_X 0.310
#define CHROMA_B_Y 0.595
#define CHROMA_C_X 0.155
#define CHROMA_C_Y 0.070
#define CHROMA_A_WEIGHT 0.2124
#define CHROMA_B_WEIGHT 0.7011
#define CHROMA_C_WEIGHT 0.0866
#define SCALE_W 1.0
#define SCAN_TYPE 1.0
#define MAX_SCAN_RATE 480.0
#define LINE_DOUBLER 0.0
#define INTER_OFF 0.0
#define FOCUS 0.5
#define ZOOM 1.0
#define COLOR_SPACE 1.0
#define CRT_CURVATURE 1.0 
#define CRT_warpX 0.031 
#define CRT_warpY 0.041
#define CRT_cornersize 0.01 
#define CRT_cornersmooth 1000.0
#endif

const vec2 corner_aspect   = vec2(1.0,  0.75);
vec2 CRT_Distortion = vec2(CRT_warpX, CRT_warpY) * 15.;


float corner(vec2 coord)
{
    coord = (coord - vec2(0.5)) + vec2(0.5, 0.5);
    coord = min(coord, vec2(1.0) - coord) * corner_aspect;
    vec2 cdist = vec2(cornersize);
    coord = (cdist - min(coord, cdist));
    float dist = sqrt(dot(coord, coord));
    
    return clamp((cdist.x - dist)*cornersmooth, 0.0, 1.0);
}


vec2 Warp(vec2 texCoord){
  vec2 curvedCoords = texCoord * 2.0 - 1.0;
  float curvedCoordsDistance = sqrt(curvedCoords.x*curvedCoords.x+curvedCoords.y*curvedCoords.y);

  curvedCoords = curvedCoords / curvedCoordsDistance;

  curvedCoords = curvedCoords * (1.0-pow(vec2(1.0-(curvedCoordsDistance/1.4142135623730950488016887242097)),(1.0/(1.0+CRT_Distortion*0.2))));

  curvedCoords = curvedCoords / (1.0-pow(vec2(0.29289321881345247559915563789515),(1.0/(vec2(1.0)+CRT_Distortion*0.2))));

  curvedCoords = curvedCoords * 0.5 + 0.5;
  return curvedCoords;
}

vec3 Yrgb_to_RGB(mat3 toRGB, vec3 W, vec3 Yrgb)
{
	mat3 xyYrgb = mat3(CHROMA_A_X, CHROMA_A_Y, Yrgb.r,
	                   CHROMA_B_X, CHROMA_B_Y, Yrgb.g,
	                   CHROMA_C_X, CHROMA_C_Y, Yrgb.b);
	mat3 XYZrgb = mat3(xyY_to_XYZ(xyYrgb[0]),
	                   xyY_to_XYZ(xyYrgb[1]),
	                   xyY_to_XYZ(xyYrgb[2]));
	mat3 RGBrgb = mat3(toRGB * XYZrgb[0],
	                   toRGB * XYZrgb[1],
	                   toRGB * XYZrgb[2]);
	return vec3(dot(W, vec3(RGBrgb[0].r, RGBrgb[1].r, RGBrgb[2].r)),
	            dot(W, vec3(RGBrgb[0].g, RGBrgb[1].g, RGBrgb[2].g)),
	            dot(W, vec3(RGBrgb[0].b, RGBrgb[1].b, RGBrgb[2].b)));
} 

vec3 color(sampler2D tex, vec2 uv)
{
	vec3 rgb;
	vec3 Yrgb;

	if (TRANSFER_FUNCTION < 2.0)
		rgb = crt_linear(COMPAT_TEXTURE(tex, uv).rgb);
	else
		rgb = srgb_linear(COMPAT_TEXTURE(tex, uv).rgb);
	if (COLOR_MODE < 2.0) {
		vec3 Wrgb = vec3(LUMINANCE_WEIGHT_R,
		                 LUMINANCE_WEIGHT_G,
		                 LUMINANCE_WEIGHT_B);

		Yrgb.r = dot(Wrgb, rgb);
		Yrgb.g = 0.0;
		Yrgb.b = 0.0;
	} else if (COLOR_MODE < 3.0) {
		vec3 Wrgb = vec3(LUMINANCE_WEIGHT_R,
		                 LUMINANCE_WEIGHT_G,
		                 LUMINANCE_WEIGHT_B);
		Yrgb.r = dot(Wrgb, rgb);
		Yrgb.g = Yrgb.r;
		Yrgb.b = 0.0;
	} else {
		Yrgb.r = rgb.r;
		Yrgb.g = rgb.g;
		Yrgb.b = rgb.b;
	}

	mat3 toRGB = colorspace_rgb();
	vec3 W = vec3(CHROMA_A_WEIGHT, CHROMA_B_WEIGHT, CHROMA_C_WEIGHT);
	vec3 RGB = Yrgb_to_RGB(toRGB, W, Yrgb);

	if (SCALE_W > 0.0) {
		vec3 white = Yrgb_to_RGB(toRGB, W, WHITE);
		float G = 1.0 / max(max(white.r, white.g), white.b);

		RGB *= G;
	}
	return clamp(RGB, 0.0, 1.0); 
}

vec3 pixel(float x, float y, sampler2D tex)
{
	float yThres = (SCAN_TYPE < 2.0 && OriginalSize.y > 1.7 * MAX_SCAN_RATE / 2.0)
	               ? OriginalSize.y / 2.0
	               : OriginalSize.y;
	float scanw;
	int line;

	if (LINE_DOUBLER > 0.0 && OriginalSize.y <= MAX_SCAN_RATE / 2.0 + EPS)
		yThres *= 2.0;
	scanw = max(0.0, 2.0 * (yThres / MAX_SCAN_RATE - 0.5));
	if (SCAN_TYPE < 2.0 && OriginalSize.y > 1.7 * MAX_SCAN_RATE / 2.0) {
		int t = FrameCount % 2;
		line = int(2.0 * yThres * y + float(t));
	} else
		line = int(2.0 * yThres * y);
	line += int(INTER_OFF);

	if (any(lessThan(vec2(x, y), vec2(0.0, 0.0))) ||
	    any(greaterThan(vec2(x, y), vec2(1.0, 1.0))))
		return BLACK;
	else {
		if (line % 2 > 0)
			return mix(BLACK, color(tex, vec2(x, y)), scanw);
		else
			return color(tex, vec2(x, y));
	} 
}

vec3 render(float y, vec4 x, vec4 taps, sampler2D tex)
{
	return pixel(x.r, y, tex) * taps.r +
	       pixel(x.g, y, tex) * taps.g +
	       pixel(x.b, y, tex) * taps.b +
	       pixel(x.a, y, tex) * taps.a;
}

void main()
{
	vec2 uv = vTexCoord.xy;

	uv = (CRT_CURVATURE > 0.5) ? (Warp(uv*TextureSize.xy/InputSize.xy)*InputSize.xy/TextureSize.xy) : uv;

	// Normal coordinates
	uv = 2.0 * (uv - 0.5);
	uv /= ZOOM;
	uv = uv / 2.0 + 0.5;

	vec2 stepxy = 1.0 / SourceSize.xy;

	// Scale vertical for scanlines/interlacing
	if (SCAN_TYPE > 1.0 || OriginalSize.y < 1.7 * MAX_SCAN_RATE / 2.0)
		stepxy.y /= 2.0;	

	vec2 pos = uv + stepxy / 2.0;
	vec2 f = fract(pos / stepxy);

	if (LINE_DOUBLER > 0.0 && OriginalSize.y <= MAX_SCAN_RATE / 2.0 + EPS)
		stepxy.y /= 2.0; 

	float C = -1.0 / 3.0 * sq(FOCUS) + 5.0 / 6.0 * FOCUS;
	float B = 1.0 - 2.0 * C;

	vec4 xtaps = kernel4(1.0 - f.x, B, C);
	vec4 ytaps = kernel4(1.0 - f.y, B, C);

	// Make sure all taps added together is exactly 1.0
	xtaps /= xtaps.r + xtaps.g + xtaps.b + xtaps.a;
	ytaps /= ytaps.r + ytaps.g + ytaps.b + ytaps.a;

	vec2 xystart = (-1.5 - f) * stepxy + pos;
	vec4 x = vec4(xystart.x,
	              xystart.x + stepxy.x,
	              xystart.x + stepxy.x * 2.0,
	              xystart.x + stepxy.x * 3.0);

	vec3 col = vec3(render(xystart.y, x, xtaps, Source) * ytaps.r +
	       render(xystart.y + stepxy.y, x, xtaps, Source) * ytaps.g +
	       render(xystart.y + stepxy.y * 2.0, x, xtaps, Source) * ytaps.b +
	       render(xystart.y + stepxy.y * 3.0, x, xtaps, Source) * ytaps.a);

	col = clamp(col, 0.0, 1.0);
	col = crt_gamma(col);

	FragColor = vec4(col, 1.0);

	FragColor *= (CRT_CURVATURE > 0.5) ? corner(uv*TextureSize.xy/InputSize.xy) : 1.0;

}
#endif