neser 1.1.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
Documentation
varying vec2 v_texCoord;
varying vec2 v_txCoord;
varying vec2 v_txToPx;
varying vec2 v_dotSizeInPx;
uniform sampler2D u_texture;
uniform sampler2D u_prevFrame;
uniform sampler2D u_colorPalette;
uniform vec2 u_sourceSize;
uniform vec2 u_outputSize;

// Hardcoded defaults from gb-params.inc + gameboy.slangp
const float PIXEL_SIZE = 0.80;
const float PIXEL_SOFTNESS = 1.0;
const float PIXEL_SHAPE = 1.0;
const float SHARPENING_AMOUNT = 1.0;
const float RESPONSE_TIME = 0.33;
const float GREY_BALANCE = 3.0;
const float BASELINE_ALPHA = 0.10;
const float COLOR_TOGGLE = 0.0;
const float PALETTE = 0.0;
// Pre-computed grey balance compensation (sigmoid mode, default params)
const float AA_COMPENSATION = 1.6;

float intersect_line(float px_s, float px_e, float dot_s, float dot_e) {
    return max(min(px_e, dot_e) - max(px_s, dot_s), 0.0);
}

float intersect_rect(vec4 px, vec4 rect) {
    vec2 bl = max(px.xy, rect.xy);
    vec2 tr = min(px.zw, rect.zw);
    vec2 c = max(tr - bl, vec2(0.0));
    return c.x * c.y;
}

void main() {
    vec3 fg_source = texture2D(u_texture, v_texCoord).rgb;

    // Response time: current + 1 previous frame
    vec3 curr = abs(vec3(1.0) - texture2D(u_texture, v_texCoord).rgb);
    vec3 prev = abs(vec3(1.0) - texture2D(u_prevFrame, v_texCoord).rgb);

    // Geometric dot intersection (fullscreen mode)
    vec2 tx_i = floor(v_txCoord);
    vec2 tx_f = v_txCoord - tx_i;
    vec2 pc = (tx_f - 0.5) * v_txToPx;
    vec4 pr = vec4(pc - v_txToPx * 0.5, pc + v_txToPx * 0.5);
    vec4 dr = vec4(-v_dotSizeInPx * 0.5, v_dotSizeInPx * 0.5);

    // Rectangular coverage with sigmoid sharpening
    float xc = intersect_line(pr.x, pr.z, dr.x, dr.z) / v_txToPx.x;
    float yc = intersect_line(pr.y, pr.w, dr.y, dr.w) / v_txToPx.y;
    float ss = 10.0 / max(PIXEL_SOFTNESS, 0.001);
    float xs = 1.0 / (1.0 + exp(-ss * (xc - 0.5)));
    float ys = 1.0 / (1.0 + exp(-ss * (yc - 0.5)));
    float rect_cov = mix(xc * yc, xs * ys, SHARPENING_AMOUNT);

    // Circular coverage with sigmoid
    float cl = intersect_rect(pr, dr) / (v_txToPx.x * v_txToPx.y);
    float cs = 1.0 / (1.0 + exp(-ss * (cl - 0.5)));
    float circ_cov = mix(cl, cs, SHARPENING_AMOUNT);

    float is_on_dot = mix(circ_cov, rect_cov, PIXEL_SHAPE);

    // Response time blending
    vec3 input_rgb = curr;
    input_rgb += (prev - input_rgb) * RESPONSE_TIME;

    // Brightness (simple mode)
    float brightness = input_rgb.r + input_rgb.g + input_rgb.b;
    float grey_adj = GREY_BALANCE / AA_COMPENSATION;
    float alpha = brightness / grey_adj + BASELINE_ALPHA;

    // Foreground color
    vec3 fg_color = texture2D(u_colorPalette, vec2(0.75, 0.5)).rgb;
    vec4 out_color;
    if (COLOR_TOGGLE < 0.5)
        out_color = vec4(fg_color, alpha);
    else
        out_color = vec4(fg_source, alpha);

    out_color.a *= is_on_dot;
    gl_FragColor = out_color;
}