Replicating Mednafen's 'Goat' shader in Retroarch

Hey. So, Mednafen’s Goat shader is pretty much exactly what I’m looking for for MegaDrive/Genesis games (and most 16/32-bit consoles), but I can’t seem to replicate it using Retroarch’s shader library. The closest I’ve managed is to use crt-easymode or crt-geom with the geometry turned off and horizontal scanlines turned down, and tweaking some other bits, but it’s just not quite right.

Road Rash II - Mednafen - Goat OpenGL + scanlines (goatron mask pattern)

Road Rash II - Retroarch (Genesis Plus GX core) - crt-easymode

The big difference is that Goat gives a more fuzzy effect, and crt-easymode has an overbright luminescence/bloom.

Are there any existing shaders that can replicate that sweet, sweet Mednafen look in Retroarch?

shaders

i asked this once when goat shader was included. i think @hunterk has best know-how of the shaders

Does anyone have a link for Mednafen’s GOAT shader (the actual code) because that would help alot? I’ve been looking and I can’t find it, their github just has pre-compiled versions of the emulator, which makes this alot more difficult (for a multitude of reasons).

Apologies if this is not what you’re looking for, but I found the Mednafen shader C++ file on the OpenEmu github:

1 Like

Yeahhhhh… That’s close to what I was looking for, it seems like it’s a combo of that link and the shader.h, alongside a couple of files (that I haven’t tried to search for, on mobile and it’s having none of github).

This is over my head though, but having these links makes it easier for anyone thats willing/can to try and port it. Sorry, if I got your hopes up…

@hunterk can I cry about it? lmfao :joy:

So, instead of trying to decode the GOAT shader settings using Mednafen’s C++ & .h files, I went along and changed some parameters of the much-loved crt-easymode to give, what I think, is a lovely replication of the CRT displays I remember in my youth, where individual pixels were visible, and it was less scanline-intensive. I grew up with PAL games, using a big-ass SONY (Grundig? Ferguson?) aperture-grille (Trinitron?) woodgrain CRT television set, where scanlines really weren’t much of a thing, but pixel definition was particularly acute. But I digress.

I use this as a general preset for my arcade and 8/16-bit console emulators.

Arcade cabinet settings:

parameters = "SHARPNESS_H;SHARPNESS_V;MASK_STRENGTH;MASK_DOT_WIDTH;MASK_DOT_HEIGHT;MASK_STAGGER;MASK_SIZE;SCANLINE_STRENGTH;SCANLINE_BEAM_WIDTH_MIN;SCANLINE_BEAM_WIDTH_MAX;SCANLINE_BRIGHT_MIN;SCANLINE_BRIGHT_MAX;SCANLINE_CUTOFF;GAMMA_INPUT;GAMMA_OUTPUT;BRIGHT_BO OST;DILATION"
SHARPNESS_H = "0.500000"
SHARPNESS_V = "1.000000"
MASK_STRENGTH = "0.300000"
MASK_DOT_WIDTH = "1.000000"
MASK_DOT_HEIGHT = "1.000000"
MASK_STAGGER = "0.000000"
MASK_SIZE = "1.000000"
SCANLINE_STRENGTH = "1.000000"
SCANLINE_BEAM_WIDTH_MIN = "1.500000"
SCANLINE_BEAM_WIDTH_MAX = "1.500000"
SCANLINE_BRIGHT_MIN = "0.350000"
SCANLINE_BRIGHT_MAX = "0.650000"
SCANLINE_CUTOFF = "400.000000"
GAMMA_INPUT = "2.000000"
GAMMA_OUTPUT = "1.800000"
BRIGHT_BOOST = "1.200000"
DILATION = "1.000000"

8/16-bit console settings:

parameters = "SHARPNESS_H;SHARPNESS_V;MASK_STRENGTH;MASK_DOT_WIDTH;MASK_DOT_HEIGHT;MASK_STAGGER;MASK_SIZE;SCANLINE_STRENGTH;SCANLINE_BEAM_WIDTH_MIN;SCANLINE_BEAM_WIDTH_MAX;SCANLINE_BRIGHT_MIN;SCANLINE_BRIGHT_MAX;SCANLINE_CUTOFF;GAMMA_INPUT;GAMMA_OUTPUT;BRIGHT_BOOST;DILATION"
SHARPNESS_H = "0.750000"
SHARPNESS_V = "0.750000"
MASK_STRENGTH = "0.500000"
MASK_DOT_WIDTH = "1.000000"
MASK_DOT_HEIGHT = "1.000000"
MASK_STAGGER = "1.000000"
MASK_SIZE = "1.000000"
SCANLINE_STRENGTH = "0.350000"
SCANLINE_BEAM_WIDTH_MIN = "2.000000"
SCANLINE_BEAM_WIDTH_MAX = "1.000000"
SCANLINE_BRIGHT_MIN = "0.250000"
SCANLINE_BRIGHT_MAX = "0.500000"
SCANLINE_CUTOFF = "225.000000"
GAMMA_INPUT = "2.000000"
GAMMA_OUTPUT = "2.000000"
BRIGHT_BOOST = "1.240000"
DILATION = "1.000000"

I’m sorta happy with these settings. They’re not as similar to Mednafen’s Goatron as I’d like, but they hit the aperture-grille nostalgia mark for me, so until such time as the GOAT shader can be 100% recreated in Retroarch, I’ll be using these settings, instead :wink:

1 Like

Which video drivers are you using? I’m using gl and crt-easymode within shaders_glsl. Other shaders sometimes don’t have options under different drivers, so try switching to glslang shaders if you haven’t already - the options are there!

Still trying after all these years!

I’m currently using the MegaBezels Guest-DrVenom shaders for the cool reflections they provide, on everything from Atari 2700 to DreamCast, but no settings I try manage to recreate the GOATRON shader in mednafen …!

Here’s as close as I get:

image

The black in Goatron is much thicker and colours bleed more into the surrounding pixels. The white in MegaBezel is much brighter and fatter. The horizontal scanlines are also less pronounced in Goatron, and vertical lines between pixels are more visible. Fiddling with the parameters in RetroArch begins to bork the overall picture until there’s no way back but to reset the shader entirely, hence my frustration at being incapable of getting the nuances right!

Scanning the mednafen sourcecode I can see Goatron’s mask width=3 and height=1, but that’s about all I can glean, not being a codemonkey.

Also, the pixels in RetroArch seem wider? Though that’s probably down to me skewing the picture to fit the platform-specific screen overlays I’ve made (e.g.:

)

Other than that, I’m at a loss. There are so many variables in MegaBezel, it’s impossible to work out which ones tend towards aligning with the Goatron filter, and narrowing down the following parameters usually results in a stupidly warped and ruined picture:

Any help? Any better RetroArch filters I could be using?

Thanks!!

2 Likes

Maybe try working on crt-guest-advanced without the mega bezel involved, just to cut down your number of options. You might also check some of the other near-infinitely-configurable CRT shaders, like crt-geom-deluxe, mame_hlsl and crt-royale.

2 Likes

Yeah I would recommend this too, if the number of parameters is overwhelming.

If you find settings you like then you can use all these in the Mega Bezel later if you want.

3 Likes

This looks like you’re quite close already. I’m seeing an RGB phosphor pattern on the image on the left but the one on the right seems to be using a B&W Mask.

So you can start there. Masks are WYSIWYG so you can get close to your screen and simply go through the CRT Mask Types until you find one that looks similar.

This looks like an Aperture Grill Mask as well so you can start from Mask 5 and go all the way up to Mask 12, keeping the Mask Size at 1.

Be sure your Mask Strength is at least 50% or you may not be able to see the Mask colours clearly.

You can take a look at the following posts for some more information on the various Masks available in CRT-GUEST-ADVANCE and HSM Mega Bezel Reflection Shader.

2 Likes

So, for those that have been holding their breath for the past 3 years waiting fervently for this topic to update, you may breathe easy: I* ended up converting Mednafen’s (https://mednafen.github.io/) shader code (src/drivers/shader.cpp & shader.h) to a slang file.

*and by I, I mean spending 2 hours with claude.ai trying to get it to work

goat.slang
#version 450
// I've tried recreating Mednafen's GOATRON shader from https://mednafen.github.io/ 
// using stuff I (and, let's face it, Claude.ai) gleaned from 
// mednafen/src/drivers/shader.cpp + shader.h
// because it's my favourite CRT shader 
// and I've had zero luck replicating using other shaders 

#pragma parameter GOAT_HDIV "RGB horizontal divergence"                      0.50 -2.0 2.0 0.01
#pragma parameter GOAT_VDIV "RGB vertical divergence"                        0.50 -2.0 2.0 0.01
#pragma parameter GOAT_TP   "Mask transparency"                              0.50  0.0 1.0 0.01
#pragma parameter GOAT_SLEN "Scanlines (0=off 1=on)"                         1.0   0.0 1.0 1.0
#pragma parameter GOAT_PAT  "Mask (0=Borg 1=Goatron 2=GoatronPrime 3=Slenderman)" 1.0 0.0 3.0 1.0

layout(set = 0, binding = 0, std140) uniform UBO {
    mat4 MVP;
    vec4 OutputSize;
    vec4 OriginalSize;
    vec4 SourceSize;
    uint FrameCount;
} global;

layout(push_constant) uniform Push {
    float GOAT_HDIV;
    float GOAT_VDIV;
    float GOAT_TP;
    float GOAT_SLEN;
    float GOAT_PAT;
} params;

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

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

#pragma stage fragment
layout(location = 0) in  vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;

layout(set = 0, binding = 2) uniform sampler2D Source;

// ---------------------------------------------------------------------------
// Mask computation - reproduces UpdateGoatMask() procedurally.
// di: output pixel coord modulo mask dimensions (ivec2(gl_FragCoord) % MaskDim)
// Returns per-pixel RGB mask vec3.
// ---------------------------------------------------------------------------
vec3 compute_mask(ivec2 di, float tp, int pat) {

    int  jx       = di.x;
    int  jy_raw   = di.y;
    bool in_black = false;
    int  lit_ch;   // which RGB channel is lit (0=R,1=G,2=B); stride=3 or 4

    if (pat == 1) {
        // GOATRON: mask_w=3 mask_h=1, hblack=false, vblack=false
        // jy irrelevant (mask_h=1), stride=3
        lit_ch   = jx - (jx / 3) * 3;   // jx % 3
        in_black = false;
    }
    else if (pat == 2) {
        // GOATRONPRIME: mask_w=4 mask_h=1, hblack=true, vblack=false
        lit_ch   = jx - (jx / 4) * 4;   // jx % 4
        in_black = (jx & 3) == 3;
    }
    else if (pat == 3) {
        // SLENDERMAN: mask_w=20 mask_h=10, hblack=true, vblack=true
        // jx = di.x % 20, jy = (10-1) - (di.y % 10) = 9 - (di.y%10)
        jx          = di.x - (di.x / 20) * 20;
        int jy      = 9 - (jy_raw - (jy_raw / 10) * 10);
        lit_ch      = jx - (jx / 4) * 4;
        int vcheck  = (jy + (jx / 4) * 2) - ((jy + (jx / 4) * 2) / 5) * 5;
        in_black    = ((jx & 3) == 3) || (vcheck == 4);
    }
    else {
        // BORG (pat==0): mask_w=8 mask_h=4, hblack=true, vblack=true
        // jx = di.x % 8, jy = 3 - (di.y % 4)
        jx          = di.x - (di.x / 8) * 8;
        int jy      = 3 - (jy_raw - (jy_raw / 4) * 4);
        lit_ch      = jx - (jx / 4) * 4;
        // last_row: (mask_h >> (jx >= mask_w/2)) - 1
        // mask_h=4: jx<4 -> (4>>0)-1=3, jx>=4 -> (4>>1)-1=1
        int last_row = (jx < 4) ? 3 : 1;
        in_black    = ((jx & 3) == 3) || (jy == last_row);
    }

    if (in_black)
        return vec3(tp);

    // lit_ch==i → 1.0, else tp. Use equal() to avoid dynamic branching.
    ivec3 ch = ivec3(lit_ch) - ivec3(0, 1, 2);       // 0 when channel matches
    vec3 sel = vec3(equal(ch, ivec3(0, 0, 0)));       // 1.0 where lit_ch==channel
    return mix(vec3(tp), vec3(1.0), sel);
}

// ---------------------------------------------------------------------------
void main() {
    // --- Sharpening factors (original: max(1.0, destDim/srcDim * 0.25)) ---
    float XSharp = max(1.0, global.OutputSize.x * global.SourceSize.z * 0.25);
    float YSharp = max(1.0, global.OutputSize.y * global.SourceSize.w * 0.25);

    // --- Texel index space (origin at texel centre, matching original) ---
    vec2 texelIndex = vTexCoord * global.SourceSize.xy - 0.5;

    // --- RGB horizontal divergence (in texel units) ---
    // Original: TexXCoordAdj = tw * (1/destW) * hdiv = (SourceSize.x/OutputSize.x)*hdiv
    float xAdj = global.SourceSize.x * global.OutputSize.z * params.GOAT_HDIV;
    vec3 txX = vec3(texelIndex.x - xAdj, texelIndex.x, texelIndex.x + xAdj);

    // --- Sharpened bilinear on X ---
    vec3 txIntX   = floor(txX);
    vec3 txFractX = clamp((txX - txIntX - 0.5) * XSharp, -0.5, 0.5) + 0.5;
    txX = (txFractX + txIntX + 0.5) * global.SourceSize.z;  // to UV

    // --- RGB vertical divergence ---
    // Original: TexYCoordAdj = vec3(-ycab, -ycab/2, +ycab)
    // where ycab = th*(1/destH)*vdiv = (SourceSize.y/OutputSize.y)*vdiv
    float ycab = global.SourceSize.y * global.OutputSize.w * params.GOAT_VDIV;
    vec3 txY = vec3(-ycab, -ycab * 0.5, ycab) + texelIndex.y;

    // --- Sharpened bilinear on Y (with scanline fract captured before sharpening) ---
    vec3 txIntY   = floor(txY);
    vec3 txFractY = txY - txIntY - 0.5;  // -0.5..+0.5, 0 at row boundary, ±0.5 at centre

    // Scanline weight: min(abs(fract)*2, 1)*0.40 + 0.60
    // Range 0.60 (dark, at row boundary fract=0) to 1.00 (bright, at row centre fract=±0.5)
    vec3 slmul = vec3(1.0);
    if (params.GOAT_SLEN > 0.5)
        slmul = min(abs(txFractY) * 2.0, 1.0) * 0.40 + 0.60;

    txFractY = clamp(txFractY * YSharp, -0.5, 0.5) + 0.5;
    txY = (txFractY + txIntY + 0.5) * global.SourceSize.w;  // to UV

    // --- Sample one channel per tap, exactly as original ---
    vec3 smoodged = vec3(
        texture(Source, vec2(txX.r, txY.r)).r,
        texture(Source, vec2(txX.g, txY.g)).g,
        texture(Source, vec2(txX.b, txY.b)).b
    );

    // --- Shadow mask ---
    ivec2 fc  = ivec2(gl_FragCoord);
    int   pat = int(params.GOAT_PAT + 0.5);
    vec3  mask;

    if (pat == 1) {
        ivec2 di = ivec2(fc.x - (fc.x / 3) * 3, 0);
        mask = compute_mask(di, params.GOAT_TP, 1);
    } else if (pat == 2) {
        ivec2 di = ivec2(fc.x - (fc.x / 4) * 4, 0);
        mask = compute_mask(di, params.GOAT_TP, 2);
    } else if (pat == 3) {
        ivec2 di = ivec2(fc.x - (fc.x / 20) * 20, fc.y - (fc.y / 10) * 10);
        mask = compute_mask(di, params.GOAT_TP, 3);
    } else {
        ivec2 di = ivec2(fc.x - (fc.x / 8) * 8, fc.y - (fc.y / 4) * 4);
        mask = compute_mask(di, params.GOAT_TP, 0);
    }

    // --- Gamma linearise, apply mask + scanlines, re-gamma ---
    smoodged  = pow(smoodged, vec3(2.2));
    smoodged *= mask;
    smoodged *= slmul;
    smoodged  = pow(smoodged, vec3(1.0 / 2.2));

    FragColor = vec4(smoodged, 1.0);
}

It’s done the trick! I appreciate the pool of users who a.) love Mednafen’s Goatron simple CRT shader as much as I do, b.) prefer using RetroArch, and c.) only wish there were a way of recreating it within RetroArch is vanishingly small, but maybe someone who fits all 3 categories will happen across this in the future and realise their prayers have been (finally!) answered.

The aspect ratio seems a little out. I used ‘core provided’ in RetroArch, which is giving it widescreen vibes.

But yeah. Next step; work out how to use my 1-pass shader within Mega Bezel’s pipeline, if such a thing is doable

3 Likes

getting it into the Mega Bezel is a big task, but getting it into uBorder is much easier (and may even work with a simple append of the append-able preset)

3 Likes