Converting Z-Fast Composite to Slang

Hi! In the last few days I’ve been setting up the cores I want to use and testing shaders that offer a good representation of the CRT output. I’m doing this in an Android phone. Small screen, 1080p, Exynos SOC… so my expectations are not great.

In a foolish attempt to put nostalgia back into my games, in the cores that provide it, I’m using Blargg filters along with some shaders. It works fine mostly, with the exception being 32bit and up, that already take up most of my phones resources simply by running, without much room for shaders, performance speaking.

Among the lightweight shaders, the one that stuck with me was zfast-composite, a GLSL shader that somehow miraculously outputs a remarkable result without almost any overhead. It’s fine even with non integer scaling.

Cutting a long story short-ish, some cores run better with Vulkan on Android and this shader has no slang version. Also, the slang zfast implementation seems to be fundamentally different, at last in my screen.

This is zfast-composite (GLSL). The individual “phosphors” are all organic, growing and shrinking like they should in my childhood CRT. Disregard the banding, it’s the exaggerated settings causing it.

This is zfast (Slang). I assume my screen resolution isn’t enough to show the mask in this one.

This is zfast-hd (Slang). Here the phosphors and the spaces between scanlines are too well defined.

I’ve cranked up the settings to maximize the dynamic beam width (equivalent settings in all three shots), and the composite has some convergence going on, but it should serve as a comparison anyway.

Is there a way to convert the GLSL version into Slang? I’m a developer also but apart from recognizing the syntax, shaders are very outside of my knowledge. Even so, if there’s anything I can do to help, just ask!

(Sorry for my broken english, I’m not a native speaker)

1 Like

looking at the 2 shaders’ code, they appear to be identical, as far as I can tell.

That’s… odd. I’ve tried the zfast-composite without any convergence and it doesn’t seem to have any effect on the phospor shape whatsoever, but I guess that was predictable.

Could the differences be caused by the way my GPU renderizes the shader through GL and Vulkan? Or the mask itself? Regardless, I couldn’t find convergence options in the two zfast slang shaders, that would be a great addition in my opinion.

By the way, this is the source picture for the tests. I’m using the FFmpeg core. Could the core be the culprit? :thinking:

Color-Genesis

It should be fine to load that image for testing.

zfast_crt_composite.glsl is an entirely different shader that doesn’t have a slang equivalent currently, but the regular zfast-crt shaders should be identical codewise, if not visually.

1 Like

I’m relieved that I was not entirely wrong. The code for the main zfast shader in both languages really seems to use the same math albeit using a different syntax, but I couldn’t wrap my head around the composite variant.

I’ll take some time to study the slang syntax and try to convert the composite shader. Wish me luck!

2 Likes

I’ve made a crude conversion using as example the main zfast shaders to get a general understanding about the params, variables and typing. The shader unfortunately fails to load and I’m apparently failing to generate logs, despite having them at debug level. Debug output for shader compilation goes to a different place?

This is the preliminary code if anyone is interested. It certainly needs fixing but without debugging it’s not easy.

#version 450

layout(push_constant) uniform Push
{
    vec4 SourceSize;
    vec4 OriginalSize;
    vec4 OutputSize;
    uint FrameCount;
float pi, blurx, blury, HIGHSCANAMOUNT1, HIGHSCANAMOUNT2, MASK_DARK, MASK_FADE, sat;
} params;

#pragma parameter blurx "Convergence X-Axis" 0.45 -1.0 2.0 0.05
#pragma parameter blury "Convergence Y-Axis" -0.25 -1.0 1.0 0.05
#pragma parameter HIGHSCANAMOUNT1 "Scanline Amount (Low)" 0.3 0.0 1.0 0.05
#pragma parameter HIGHSCANAMOUNT2 "Scanline Amount (High)" 0.2 0.0 1.0 0.05
#pragma parameter MASK_DARK "Mask Effect Amount" 0.25 0.0 1.0 0.05
#pragma parameter MASK_FADE "Mask/Scanline Fade" 0.8 0.0 1.0 0.05
#pragma parameter sat "Saturation" 1.1 0.0 3.0 0.05

#define pi 3.14159
#define blurx params.blurx
#define blury params.blury
#define HIGHSCANAMOUNT1 params.HIGHSCANAMOUNT1
#define HIGHSCANAMOUNT2 params.HIGHSCANAMOUNT2
#define BRIGHTBOOST params.BRIGHTBOOST
#define MASK_DARK params.MASK_DARK
#define MASK_FADE params.MASK_FADE
#define sat params.sat

layout(std140, set = 0, binding = 0) uniform UBO
{
    mat4 MVP;
} global;

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
layout(location = 1) out float maskFade;

void main()
{
    gl_Position = global.MVP * Position;
    vTexCoord = TexCoord;
    maskFade = 0.3333*MASK_FADE;
}


#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in float maskFade;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;

void main()
{
    vec2 pos = TexCoord.xy;
    
    vec3 sample1 = texture(Source,vec2(pos.x + blurx/1000.0, pos.y - blury/1000.0)).rgb;
    vec3 sample2 = texture(Source,pos).rgb;
    vec3 sample3 = texture(Source,vec2(pos.x - blurx/1000.0, pos.y + blury/1000.0)).rgb;
    
    vec3 colour = vec3 (sample1.r*0.5+sample2.r*0.5, sample1.g*0.25 + sample2.g*0.5 + sample3.g*0.25, sample2.b*0.5 + sample3.b*0.5);
    float lum = colour.r*0.4 + colour.g*0.4 + colour.b*0.2;
    
    vec3 lumweight=vec3(0.3,0.6,0.1);
    float gray = dot(colour,lumweight);
    vec3 graycolour = vec3(gray);

    //Gamma-like
    colour*=mix(0.4,1.0,lum);    
    
    float SCANAMOUNT = mix(HIGHSCANAMOUNT1,HIGHSCANAMOUNT2,lum);
    float scanLine =  SCANAMOUNT * sin(2.0*pi*pos.y*TextureSize.y);
    
    float whichmask = fract(gl_FragCoord.x*-0.4999);
    float mask = 1.0 + float(whichmask < 0.5) * -MASK_DARK;

    //Gamma-like 
    colour*=mix(2.0,1.0,lum);    
    
    colour = vec3(mix(graycolour,colour.rgb,sat));

    colour.rgb *= mix(mask*(1.0-scanLine), 1.0-scanLine, dot(colour.rgb,vec3(maskFade)));
    FragColor.rgb = colour.rgb;
}
1 Like

Hmm, shader errors should print to log or console.

I took a look and you were very close. The very first line of the fragment just needed vTexCoord instead of TexCoord and line 74 had a TextureSize.y that needed to be params.SourceSize.y

Here’s the end result

#version 450

layout(push_constant) uniform Push
{
    vec4 SourceSize;
    vec4 OriginalSize;
    vec4 OutputSize;
    uint FrameCount;
float pi, blurx, blury, HIGHSCANAMOUNT1, HIGHSCANAMOUNT2, MASK_DARK, MASK_FADE, sat;
} params;

#pragma parameter blurx "Convergence X-Axis" 0.45 -1.0 2.0 0.05
#pragma parameter blury "Convergence Y-Axis" -0.25 -1.0 1.0 0.05
#pragma parameter HIGHSCANAMOUNT1 "Scanline Amount (Low)" 0.3 0.0 1.0 0.05
#pragma parameter HIGHSCANAMOUNT2 "Scanline Amount (High)" 0.2 0.0 1.0 0.05
#pragma parameter MASK_DARK "Mask Effect Amount" 0.25 0.0 1.0 0.05
#pragma parameter MASK_FADE "Mask/Scanline Fade" 0.8 0.0 1.0 0.05
#pragma parameter sat "Saturation" 1.1 0.0 3.0 0.05

#define pi 3.14159
#define blurx params.blurx
#define blury params.blury
#define HIGHSCANAMOUNT1 params.HIGHSCANAMOUNT1
#define HIGHSCANAMOUNT2 params.HIGHSCANAMOUNT2
#define BRIGHTBOOST params.BRIGHTBOOST
#define MASK_DARK params.MASK_DARK
#define MASK_FADE params.MASK_FADE
#define sat params.sat

layout(std140, set = 0, binding = 0) uniform UBO
{
    mat4 MVP;
} global;

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
layout(location = 1) out float maskFade;

void main()
{
    gl_Position = global.MVP * Position;
    vTexCoord = TexCoord;
    maskFade = 0.3333*MASK_FADE;
}


#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in float maskFade;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;

void main()
{
    vec2 pos = vTexCoord.xy;
    
    vec3 sample1 = texture(Source,vec2(pos.x + blurx/1000.0, pos.y - blury/1000.0)).rgb;
    vec3 sample2 = texture(Source,pos).rgb;
    vec3 sample3 = texture(Source,vec2(pos.x - blurx/1000.0, pos.y + blury/1000.0)).rgb;
    
    vec3 colour = vec3 (sample1.r*0.5+sample2.r*0.5, sample1.g*0.25 + sample2.g*0.5 + sample3.g*0.25, sample2.b*0.5 + sample3.b*0.5);
    float lum = colour.r*0.4 + colour.g*0.4 + colour.b*0.2;
    
    vec3 lumweight=vec3(0.3,0.6,0.1);
    float gray = dot(colour,lumweight);
    vec3 graycolour = vec3(gray);

    //Gamma-like
    colour*=mix(0.4,1.0,lum);    
    
    float SCANAMOUNT = mix(HIGHSCANAMOUNT1,HIGHSCANAMOUNT2,lum);
    float scanLine =  SCANAMOUNT * sin(2.0*pi*pos.y*params.SourceSize.y);
    
    float whichmask = fract(gl_FragCoord.x*-0.4999);
    float mask = 1.0 + float(whichmask < 0.5) * -MASK_DARK;

    //Gamma-like 
    colour*=mix(2.0,1.0,lum);    
    
    colour = vec3(mix(graycolour,colour.rgb,sat));

    colour.rgb *= mix(mask*(1.0-scanLine), 1.0-scanLine, dot(colour.rgb,vec3(maskFade)));
    FragColor.rgb = colour.rgb;
}
1 Like

Perfect! Thanks for the help, it works!

I don’t know the process to add a shader to the slang repo, nor if the original author allows a conversion of this type to be published, but in my opinion this version surely deserves to be available to more people.

2 Likes