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