Guide To Pixel Perfect Scanlines

So what is this all about? I dislike all the built in shaders for Retroarch (which is what RetroPie is based off of) and I especially dislike them on the Picade as I’m so close to the screen and the screen is relatively low res. Essentially they all blur the image and/or introduce moire artifacts. Dont get me wrong some of the shaders on libretro amaze me in their complexity but I’m just not a fan.

I mean why try to emulate a CRT screen when you’re never going to get there, at least not on a low res screen (1080p and below).

There are some features I like from CRT’s, primarily the scanlines.

I have a Sony PVM, with a fully decked out MiSTer FPGA and original controllers and I love it but there are problems with such a setup such as vertical arcades, CRT’s weigh a ton and it’s a lot of work maintaining them - I’m still working up to recapping mine. Its just nice to have a fallback and its nice to have a Picade on your lap.

The thing is as you go up the PVM models and then onto the BVMs, Sony was trying to get as sharp an image as possible (ie as sharp scanlines as possible) and as straighter screen geometry possible. These are both what a Picade’s LCD screen excels at. What the Picade LCD does not excel at are resolutions that are not native and to a lesser extent brightness. We’ll come back to brightness later.

What do I want then? Something dead simple.

I just want to use an integer scaling and either ‘overscan’ or ‘underscan’ the image (to use CRT terminology) - whichever is subjectively preferable. I want this with simple whole pixel scanlines that are surrounded by black pixels.

So I created this shader that produces the above for all the different resolutions that the various cores and games use.

integer_scanlines.glsl:

#pragma parameter SCANLINE_WIDTH "Scanline Width" 1.0 0.0 8.0 1.0
#pragma parameter SCREEN_WIDTH "Screen Width" 1024.0 0.0 7680.0 1.0
#pragma parameter SCREEN_HEIGHT "Screen Height" 768.0 0.0 4320.0 1.0


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

#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float SCANLINE_WIDTH;
uniform COMPAT_PRECISION float SCREEN_WIDTH;
uniform COMPAT_PRECISION float SCREEN_HEIGHT;
#else
#define SCANLINE_WIDTH 	1.0
#define SCREEN_WIDTH 	1024.0
#define SCREEN_HEIGHT 	768.0
#endif

// GLSL shader autogenerated by cg2glsl.py.

#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

COMPAT_VARYING     float _frame_rotation;
COMPAT_VARYING     vec4 _color1;
struct output_dummy {
    vec4 _color1;
};
struct input_dummy {
    vec2 _video_size;
    vec2 _texture_size;
    vec2 _output_dummy_size;
    float _frame_count;
    float _frame_direction;
    float _frame_rotation;
};
vec4 _oPosition1;
vec4 _r0005;
COMPAT_ATTRIBUTE vec4 VertexCoord;
COMPAT_ATTRIBUTE vec4 COLOR;
COMPAT_ATTRIBUTE vec4 TexCoord;
COMPAT_VARYING vec4 COL0;
COMPAT_VARYING vec4 TEX0;
COMPAT_VARYING float Scale;
 
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;
    _r0005 = VertexCoord.x*MVPMatrix[0];
    _r0005 = _r0005 + VertexCoord.y*MVPMatrix[1];
    _r0005 = _r0005 + VertexCoord.z*MVPMatrix[2];
    _r0005 = _r0005 + VertexCoord.w*MVPMatrix[3];
    _oPosition1 = _r0005;
    _oColor = COLOR;
    _otexCoord = TexCoord.xy;
    gl_Position = _r0005;
    COL0 = COLOR;
    TEX0.xy = TexCoord.xy;

    vec2 ScreenSize = max(OutputSize, vec2(SCREEN_WIDTH, SCREEN_HEIGHT));

    if((InputSize.x > ScreenSize.x) || (InputSize.y > ScreenSize.y))
    {
	Scale = 1.0;
    }
    else 
    {
        float ScaleFactor = 2.0;
        
        while(((InputSize.x * ScaleFactor) <= ScreenSize.x) && ((InputSize.y * ScaleFactor) <= ScreenSize.y))
    	{
	    ScaleFactor += 1.0;
        }

	Scale = ScaleFactor - 1.0;
    }
} 
#elif defined(FRAGMENT)

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

COMPAT_VARYING     float _frame_rotation;
COMPAT_VARYING     vec4 _color;
struct output_dummy {
    vec4 _color;
};
struct input_dummy {
    vec2 _video_size;
    vec2 _texture_size;
    vec2 _output_dummy_size;
    float _frame_count;
    float _frame_direction;
    float _frame_rotation;
};
uniform sampler2D Texture;
COMPAT_VARYING vec4 TEX0;
COMPAT_VARYING float Scale;
 
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;


float mod_integer(float a, float b) 
{
    float m = a - floor((a + 0.5) / b) * b;
    return floor(m + 0.5);
}

void main()
{
    output_dummy _OUT;

    vec2 InPixels = (TEX0.xy * TextureSize) * vec2(Scale);

    if(mod_integer(floor(InPixels.y), Scale) < SCANLINE_WIDTH)
    {
        _OUT._color = COMPAT_TEXTURE(Texture, TEX0.xy);
    }
    else
    {
        _OUT._color = vec4(0.0,0.0,0.0,1.0);
    }

    FragColor = _OUT._color;
    return;
} 
#endif

I’ll explain how to use this in a reply but it must be used with integer scaling and ideally in a darkened room to make up for the lack of brightness.

Here’s a close up picture of what it looks like in Street Fighter II with 3x integer scale in both directions:


(Moire effect above is due to me taking a picture with my phone of the Picade screen and then halving the images resolution in this post)

Setup
Download and install WinSCP.

Create two text files, one containing the shader code copied from above in a file called integer_scanlines.glsl and the other in a file called integer_scanlines.glslp containing this:

shaders = "1"
shader0 = "shaders/integer_scanlines.glsl"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "false"
srgb_framebuffer0 = "false"

Boot your picade and find out its IP address via the menu Retropie->Show IP.

Open up WinSCP and Session->New Session->New Site
File Protocol: SFTP, Host Name: IP address you just found, Port No.: 22, User Name: pi Password: raspberry
Login

Copy the two files you just created to: /opt/retropie/configs/all/retroarch/shaders
Then move integer_scanlines.glsl to /opt/retropie/configs/all/retroarch/shaders/shaders

Now open up /opt/retropie/configs/all/retroarch.cfg and change:

video_scale = 3.0
video_scale_integer = true
video_force_aspect = true
aspect_ratio_index = 21
video_shader = "/opt/retropie/emulators/retroarch/shader/integer_scanlines.glslp"
video_shader_enable = true

Optionally you may need:

video_windowed_fullscreen = true
video_fullscreen = true
video_fullscreen_x = 1024
video_fullscreen_y = 768

Save and reboot your picade.

This should set mostly everything up and you wont need WinSCP anymore.

However you may want to setup custom viewports, change the scanline widths and if you have a different screen resolution than the one that came with the Picade of 1024x768 then you can change this too in the shader parameters.

Custom Viewport:

Enter game then enter the Retroarch menu which on my Picade is left hand side button and middle top row button together yours may differ.
Then back up to Main Menu->Settings->Video->Scaling then there seems to a bug in this menu where you have to switch Integer Scale off then back on again then go down to Aspect Ratio and push Right twice to see ‘Custom’ and choose Custom Aspect Ratio Height and Width of 3x (or whatever you want). Then go to Main Menu->Quick Menu->Overrides->Save Game Overrides to save the custom viewport for the game.

Different Width Scanlines:

Go to Main Menu->Quick Menu->Shaders->Shader Parameters and change the Scanline Width to what you want. Then go to Main Menu->Quick Menu->Shaders->Save and Save Game Preset to save this setting for the game.

Custom Screen Resolution:

If you’ve got a custom screen for your Picade or are running this on a normal Raspberry Pi connected to a monitor/tv then go to Main Menu->Quick Menu->Shaders->Shader Parameters and change the Screen Width/Height to what your monitor/tv’s native resolution is. Then go to Main Menu->Quick Menu->Shaders->Save and Save Global Preset.

I’ve found there is a bug with Mame2003 vertical games, integer scaling and custom viewports. Basically it tells the Retroarch the wrong width and height when rotated i.e youre viewing a game on a landscape ortiented screen as happens with the Picade. This rersults in an incorrect aspect ratio and breaks this shader badly but it also breaks all other shaders and its just wrong. FBNeo does not suffer from this problem so use that for games when you can. I’m trying to figure out a work around/what fixes it.

Apologies its not a bug with Mame2003 its a bug with FBAlpha2012 as FBNeo largely supercedes it its probably not an issue.