Using CRT shader on a real CRT Monitor


blurs (and basically any other additive/subtractive effects) should ideally happen in linear space, since doing them in gamma space accentuates darker colors. Tvout-tweaks doesn’t bother with this for performance reasons (and because YIQ conversion is intended to happen in gamma space), but you can try it out here:

//	TV-out tweaks
//	Author: aliaspider - [email protected]
//	License: GPLv3

// this shader is meant to be used when running
// an emulator on a real CRT-TV @240p or @480i
// Basic settings:

// signal resolution
// higher = sharper
#pragma parameter TVOUT_RESOLUTION "TVOut Signal Resolution" 256.0 0.0 1024.0 32.0 // default, minimum, maximum, optional step

// simulate a composite connection instead of RGB
#pragma parameter TVOUT_COMPOSITE_CONNECTION "TVOut Composite Enable" 0.0 0.0 1.0 1.0

// use TV video color range (16-235)
// instead of PC full range (0-255)
#pragma parameter TVOUT_TV_COLOR_LEVELS "TVOut TV Color Levels Enable" 0.0 0.0 1.0 1.0

// Advanced settings:
// these values will be used instead
// to simulate different signal resolutions(bandwidth)
// for luma (Y) and chroma ( I and Q )
// this is just an approximation
// and will only simulate the low bandwidth anspect of
// composite signal, not the crosstalk between luma and chroma
// Y = 4MHz I=1.3MHz Q=0.4MHz
#pragma parameter TVOUT_RESOLUTION_Y "TVOut Luma (Y) Resolution" 256.0 0.0 1024.0 32.0
#pragma parameter TVOUT_RESOLUTION_I "TVOut Chroma (I) Resolution" 83.2 0.0 256.0 8.0
#pragma parameter TVOUT_RESOLUTION_Q "TVOut Chroma (Q) Resolution" 25.6 0.0 256.0 8.0

// formula is MHz=resolution*15750Hz
// 15750Hz being the horizontal Frequency of NTSC
// (=262.5*60Hz)

#if defined(VERTEX)

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

#ifdef GL_ES
#define COMPAT_PRECISION mediump

COMPAT_ATTRIBUTE vec4 VertexCoord;

vec4 _oPosition1;
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;

void main()
vec4 _oColor;
vec2 _otexCoord;
    gl_Position = VertexCoord.x * MVPMatrix[0] + VertexCoord.y * MVPMatrix[1] + VertexCoord.z * MVPMatrix[2] + VertexCoord.w * MVPMatrix[3];
    _oPosition1 = gl_Position;
    _oColor = COLOR;
    _otexCoord = TexCoord.xy;
    COL0 = COLOR;
    TEX0.xy = TexCoord.xy;

#elif defined(FRAGMENT)

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

#ifdef GL_ES
precision highp float;
precision mediump float;
#define COMPAT_PRECISION mediump

#ifdef PARAMETER_UNIFORM // If the shader implementation understands #pragma parameters, this is defined.
// Fallbacks if parameters are not supported.
#define TVOUT_RESOLUTION 256.0 // Default
#define TVOUT_RESOLUTION_Y 256.0

struct output_dummy {
    vec4 _color;

#define pi			3.14159265358
#define a(x) abs(x)
#define d(x,b) (pi*b*min(a(x)+0.5,1.0/b))
#define e(x,b) (pi*b*min(max(a(x)-0.5,-1.0/b),1.0/b))
#define STU(x,b) ((d(x,b)+sin(d(x,b))-e(x,b)-sin(e(x,b)))/(2.0*pi))
//#define X(i) (offset-(i))
#define L(C) clamp((C -16.5/ 256.0)*256.0/(236.0-16.0),0.0,1.0)
#define LCHR(C) clamp((C -16.5/ 256.0)*256.0/(240.0-16.0),0.0,1.0)

vec3 LEVELS(vec3 c0)
         return vec3(L(c0.x),LCHR(c0.y),LCHR(c0.z));
         return L(c0);
      return c0;

#define GETC(c) \
      c = (LEVELS(COMPAT_TEXTURE(Texture, vec2(TEX0.x - X*oneT,TEX0.y)).xyz) * RGB_to_YIQ); \
   else \
      c = (pow(LEVELS(COMPAT_TEXTURE(Texture, vec2(TEX0.x - X*oneT,TEX0.y)).xyz), vec3(2.2)))

#define VAL(tempColor) \
      tempColor += vec3((c.x*STU(X,(TVOUT_RESOLUTION_Y*oneI))),(c.y*STU(X,(TVOUT_RESOLUTION_I*oneI))),(c.z*STU(X,(TVOUT_RESOLUTION_Q*oneI)))); \
   else \
      tempColor += (c*STU(X,(TVOUT_RESOLUTION*oneI)))

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;

void main()
mat3 RGB_to_YIQ = mat3(0.299,0.587,0.114,
		 0.211456,-0.522591, 0.311135);

mat3 YIQ_to_RGB = mat3(1.0,0.9563,0.6210,
		 1.0,-1.1070, 1.7046);

vec3 tempColor=vec3(0.0,0.0,0.0);
float	offset	= fract((TEX0.x * TextureSize.x) - 0.5);
   float oneT=1.0/TextureSize.x;
   float oneI=1.0/InputSize.x;

   float X;
   vec3 c;

   X = (offset-(-1.0));//X(-1.0);

   X = (offset-(0.0));//X(0.0);

   X = (offset-(1.0));//X(1.0);

   X = (offset-(2.0));//X(2.0);

      tempColor= (TVOUT_COMPOSITE_CONNECTION > 0.5) ? tempColor * YIQ_to_RGB : pow(tempColor, vec3(1.0/2.2));

    output_dummy _OUT;
    _OUT._color = vec4(tempColor, 1.0);
    FragColor = _OUT._color;

I left the YIQ codepath used for the composite colors feature in gamma space, so you can really see the difference by toggling that option on and off.


Awesome! Whatever you did to TVout tweaks, I’m able to adjust signal resolution in finer increments now, and there’s a wider range of possible adjustment. Nice! :+1:

You can definitely see the difference in brightness, flipping back and forth between composite.

Is there documentation for tvout-tweaks? Just wish I understood what it’s doing a little better.

What is the reason why the other shaders I mentioned are doing their blur in gamma space? Is this by design or is it something to do with how I’ve stacked the shaders?


controlled-sharpness and sharp-bilinear shouldn’t be darkening the image unnecessarily, since they’re not doing anything after sampling. /shrug


The problem I see with linearizing after every texture fetch is that it can be considerably slower, specially on potatoes and it doesn’t look any different then the multipass alternative, like: TV Out Tweaks Linearized Multipass. The two shaders referenced to in that preset don’t exist, they both go in the directory: linear/. I post them here: srgb_to_linear.glsl and linear_to_srgb.glsl


Yeah, that works, too. I figured the performance hit was worth it for the simplicity of not adding more passes into his chain.

Is the composite color option working properly in the multipass version?


Yes :slight_smile:

EDIT: Oh, you mean “properly” because the matrices are not the “proper” ones?. Still, the difference is fairly small :stuck_out_tongue:


Yeah, those transformations look terrible if you linearize it right there in the single pass but it indeed doesn’t look too bad linearizing in a separate pass (not sure why but whatever). However, the multipass does seem to screw with the TV color levels option pretty badly… Anyway, yeah, it’s another option :slight_smile:


Well, I bet we both hoped for a solution that didn’t imply tinkering :laughing:. Anyway, here is a version that should fix all problems: tvout-tweaks-linearized-multipass

Levels is on on these screenshots:


I’m using sharp bilinear as a way to deal with the scaling artifacts that result from non-integer scaling, which reverse-AA doesn’t do. It’s been tricky getting sharp bilinear to work with some of the other shaders I’m using. So far I haven’t been able to get reverse-AA to work with the rest of the chain.


Where’s the non-integer scaling coming from? Do you mean on the horizontal axis or vertical?


Ah, should have been more specific. It’s only an issue when I play psx games. When emulating psx games, I just set a single custom aspect ratio and then let sharp bilinear or pixelate take care of the scaling artifacts that can sometimes occur.

Ideally, I’d have all of the following in a single shader chain:

  1. Interlacing (for scanlines)
  2. Image-adjustment (for gamma correction)
  3. Sharp-bilinear (for scaling artifacts)
  4. something for adjustable sharpness/blur that doesn’t darken the image
  5. An optional dithering filter. Jinc2-sharper?

It’s been tricky getting all these components to work together. I don’t mind weird/long shader chains; performance cost is a bigger concern for me.


I figured out that this causes the image to be darkened:

shader 0: interlacing
shader 0 filter: don't care
shader 0 scale: 2x
shader 1: sharp-bilinear
shader 1 filter: linear
shader 1 scale: don't care

The darkening seems to happen whenever I set the scale for interlacing to 2x, but that’s also how I’ve been getting sharp-bilinear to work at all with it.


sharp-bilinear is darkening because you are stretching the “fake scanlines” vertically, blending them together.


I’m having trouble getting this to load. I double-checked my file paths and they look good…


Ah, that makes sense, but it’s still kinda weird- the black lines themselves don’t seem to change in appearance but the visible lines between them get darker.

I’ve been setting the scale for interlacing to 2x because otherwise the scanlines just disappear when I add sharp-bilinear as the next pass. If I put interlacing as the second pass, it just undoes whatever sharp-bilinear is doing because interlacing uses nearest neighbor.


It’s not undoing because of nearest-neighbor but because the scale is implicitly 1x. You need to specify that you need the whole viewport, but I still don’t think if it’s gonna look good if you stretch vertically. Here is some example:

alias0 = ""
alias1 = ""
AUTO_PRESCALE = "1.000000"
enable_480i = "1.000000"
feedback_pass = "0"
filter_linear0 = "true"
filter_linear1 = "false"
float_framebuffer0 = "false"
float_framebuffer1 = "false"
mipmap_input0 = "false"
mipmap_input1 = "false"
parameters = "SHARP_BILINEAR_PRE_SCALE;AUTO_PRESCALE;percent;enable_480i;top_field_first"
percent = "0.000000"
scale_type_x0 = "viewport"
scale_type_x1 = "source"
scale_type_y0 = "viewport"
scale_type_y1 = "source"
scale_x0 = "1.000000"
scale_x1 = "1.000000"
scale_y0 = "1.000000"
scale_y1 = "2.000000"
shader0 = "retro/shaders/sharp-bilinear.glsl"
shader1 = "misc/interlacing.glsl"
shaders = "2"
srgb_framebuffer0 = "false"
srgb_framebuffer1 = "false"
top_field_first = "0.000000"
wrap_mode0 = "clamp_to_border"
wrap_mode1 = "clamp_to_border"

That’s weird, I just downloaded it just in case, and tested it and it worked, fine :thinking:. Any error messages in the log?


If I put inerlacing after sharp-bilinear, I can’t get sharp bilinear to do what it’s supposed to do no matter what I set the scale/filter to. If I put interlacing before sharp-bilinear, I can get it to work, but only by setting the scale for interlacing to 2x. Seems like this is the only way to get them to work together. I guess I could compensate by adjusting luminance in image-adjustment, it’d be nice to find a solution that doesn’t involve that, though. Maybe I should try experimenting with the “scanline” shader instead of interlacing.

I don’t see any log errors, but I’m not completely confident that I’m logging correctly, either. I enabled logging under advanced settings in RA, then launched RA using Retroarch (log to file) from the windows menu. Saw a bunch of warnings but no errors.

I can’t get the shaders to load individually, either.

I got the xbr horizontal+interlacing preset that you posted earlier to load fine, so I don’t know what’s going on.


Just to be sure that you have all the shaders and the right paths, this is what I have:

├── shaders
│   └── tvout-tweaks-multipass
│       ├── tvout-tweaks-pass-0.glsl
│       └── tvout-tweaks-pass-1.glsl
└── tvout-tweaks-linearized-multipass.glslp

Did you try the preset I just posted, the one with sharp-bilinear and then interlacing?. Did sharp-bilinear work as it should?. I think it did for me, but interlacing got confused by the previous scale, and it just rolls the black lines even on 240p content. I think interlacing needs 1x in vertical as the input scale to work properly.

EDIT: I suspect that the problem with the shaders not loading for you is the line endings, I’m on Linux, so it has just line feeds instead of carriage return + line feed, but I’m not sure cause I don’t have Windows handy.


hmm, yeah, looks like all the files/paths are correct. I can’t get the shaders to load individually or through the preset; nothing happens. :frowning:

Yep, sharp-bilinear does what it’s supposed to now, but now interlacing isn’t working right; the scanlines vanish and my system slows to a crawl.


tvout-tweaks’ resolution parameter can be used similar to pixellate/sharp-bilinear. At 1024, it does essentially the same thing but only on the X axis. Any lower and you shouldn’t be seeing scaling artifacts either, AFAIK. You can also use the blurring to smooth out dithering, but I assume you want the sharp/transparency look rather than blurry/smeary?

You can get gamma correction out of the multipass version by just changing the 2.2 in the last line of the first pass to 2.4 or 2.5 to taste:

FragColor = vec4(pow(LEVELS(COMPAT_TEXTURE(Source, vTexCoord).rgb), vec3(2.5)), 1.0);

Are you using a super-res? If not, try making a custom res of 3840/2560/1920x480 (whatever your GPU will play nice with), then load up RiskyJumps’ multipass tvout-tweaks (it’s in the GLSL repo now) at 1x followed by interlacing at 2x. Or, you can try this preset, which does that, plus has a pass of jinc2-sharp to blend the dithering (run it from your ‘presets’ directory):

shaders = "4"

shader0 = "../windowed/shaders/jinc2-sharp.glsl"

shader1 = "../crt/shaders/tvout-tweaks-multipass/tvout-tweaks-pass-0.glsl"
float_framebuffer1 = "true"
srgb_framebuffer1 = "true"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"

shader2 = "../crt/shaders/tvout-tweaks-multipass/tvout-tweaks-pass-1.glsl"
float_framebuffer2 = "false"
srgb_framebuffer2 = "false"
scale_type_x2 = "viewport"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"

shader3 = "../misc/interlacing.glsl"
scale_type3 = source
scale3 = 2.0


TVOUT_RESOLUTION = "256.000000"
TVOUT_RESOLUTION_I = "83.199997"
TVOUT_RESOLUTION_Q = "25.600000"
TVOUT_RESOLUTION_Y = "256.000000"
CRT_GAMMA = "2.5"

EDIT: I went ahead and added the gamma option to multipass tvout-tweaks in the repo.