The Scanline Classic Shader

UPDATE 2025-3-26: Version 10.1 now available

Features of this shader

  • All-in-one signal processing+CRT shader package
  • Supports RGB, S-Video, Composite, and RF
  • Support for NTSC, PAL, and regional variants like PAL-M (Brazil)
  • Automatic interlacing
  • Parameter system aligns with controls found on TVs/monitors
  • HDRR pipeline that smoothly handles out-of-gamut imaging
  • SDR, HDR, and WCG presets
  • Three CRT mask types and a variety of bezels
  • Full geometry simulation, including curvature on mask
  • Various presets of popular systems from the 3rd gen to the 5th gen ready to go
  • Mark III / Master System presets coming soon

Recent screenshot:

This is official support thread for my Scanline Classic shader. You can find the latest version with instructions here: https://github.com/anikom15/scanline-classic

README and parameter info below.

Scanline Classic

Copyright © 2025-2026 W. M. Martinez.

Copying and distribution of this file, with or without modification,

are permitted in any medium without royalty provided the copyright

notice and this notice are preserved. This file is offered as-is,

without any warranty.

Full, up-to-date source code is available at

A general purpose RetroArch shader with an emphasis on realism while

maintaining a high degree of flexibility and aesthetic quality.

Version 10.1

README Edition 11

Quick Start

Users

  1. Install Scanline Classic presets to your RetroArch slang shader path (typically shaders/shaders_slang/bezel/scanline-classic).

  2. In RetroArch, load a preset, then run Quick Menu > Shaders > Apply Changes.

  3. Recommended RetroArch scaling defaults: Aspect Ratio = Full, Integer Scale = Off.

Developers

  1. Build presets: python build.py

  2. Build with shader lint gate: python build.py --lint-shaders

  3. Build with strict shader-structure gate: python build.py --lint-shaders --strict-structure

Generated presets are written to out/.

Global Options

Scanline Classic ships with a global options skeleton at config/options.skel.cfg.

To use it, copy it to config/options.cfg in your Scanline Classic install, then uncomment the #define lines you want to enable.

This lets you turn on global compile-time options such as disabling bezel rendering and forcing flat geometry.

Prerequisites

  • RetroArch with Slang shader support.

  • Python 3 for local builds (python build.py).

User Performance Requirements

Use this quick guide when choosing a preset/resolution target.

| Requirement Tier | GPU Target | Target Resolution |

| — | — | — |

| Minimum | NVIDIA GeForce GTX 1050 (or equivalent) | 1280x800 |

| Recommended | NVIDIA GeForce RTX 3060 | 3840x2160 (4K) |

These are practical baseline targets for full Scanline Classic pipelines (e.g. composite, RF, glow). Lighter presets (fewer signal-processing stages, reduced effects, or no bezel) generally run faster.

Baseline Performance (Estimated, Developer Notes)

Performance depends on the selected preset, output resolution, driver stack, RetroArch settings, and emulator/core load. As a baseline reference:

  • Minimum practical target: NVIDIA GeForce GTX 1050 (or equivalent) at 1280x800

  • Recommended target: NVIDIA GeForce RTX 3060 at 3840x2160 (4K)

These estimates are anchored to the professional/famicom-av.slangp chain (16 shader passes including Rcomposite simulation, CRT, mask, and bezel), which has been verified to run full speed at 4K on an RTX 4060 Ti.

Rough scaling expectation:

  • 4K renders about 8.1x as many pixels as 1280x800.

  • The famicom-av preset is one of the heavier pipelines due to its multi-stage analog encode/decode path before CRT/output passes (RF pipelines are heavier, however).

  • Given the 4060 Ti 4K full-speed reference, a GTX 1050 at 1280x800 is a realistic minimum baseline for similar presets, while an RTX 3060 is a realistic 4K target with useful headroom.

For lighter presets (fewer signal-processing passes, no bezel, or reduced effects), expected performance is typically better than this baseline.

Preset Overview

Scanline Classic provides a wide range of presets tailored for both consumer and professional video systems. Presets are organized by system and signal type, and are found in the presets folder of your install location. Below is an overview of the available presets:

Consumer Presets

  • sfc.slangp: Super Famicom (Japan) base preset

  • snes.slangp: Super Nintendo (North America) base preset

  • snes-br.slangp: Super Nintendo (Brazil) base preset

  • snes-eu.slangp: Super Nintendo (Europe) base preset

Professional Presets

  • sfc-composite.slangp: Super Famicom (Japan) with composite video signal simulation

  • sfc-rf.slangp: Super Famicom (Japan) with RF signal simulation

  • sfc.slangp: Super Famicom (Japan) with RGB signal simulation

  • sfc-svideo.slangp: Super Famicom (Japan) with S-Video signal simulation

  • snes-composite.slangp: Super Nintendo (North America) with composite video

  • snes-svideo.slangp: Super Nintendo (North America) with S-Video

  • snes-rf.slangp: Super Nintendo (North America) with RF

  • snes-eu-composite.slangp: Super Nintendo (Europe) with composite video

  • snes-eu-rf.slangp: Super Nintendo (Europe) with RF

  • snes-gb-rf.slangp: Super Nintendo (Great Britain) with RF

  • snes-br.slangp: Super Nintendo (Brazil) professional preset

The Super Nintendo presets are representative for other systems. For other systems, the distribution included with Libretro/slang-shaders is limited to a consumer and a professional preset for each major region (Japan, America, and Europe). A full set of presets can be downloaded from the EXTRAS distribution.

Each preset is designed to closely match the characteristics of the original hardware and signal path, including colorimetry, geometry, and signal artifacts. Use these as starting points for your own customizations or as reference-quality emulation targets.

For more details on each preset and its intended use, see the corresponding .json file in presetdata/input/consumer/ or presetdata/input/professional/

in the source code distribution.

RetroArch Usage Guidance

For best results with Scanline Classic presets in RetroArch:

  • Set Aspect Ratio to 16:9 in Video > Scaling. This ensures correct geometry for most presets and modern displays.

  • Set Integer Scale to Off. This allows the shader’s geometry and curvature controls to work as intended and avoids unwanted cropping or pillarboxing.

  • After loading a preset, go to Quick Menu > Shaders > Apply Changes to activate it.

  • To save your configuration for future sessions, use Quick Menu > Shaders > Save > Save Game Preset (for per-game) or Save Core Preset (for all games on the current core).

These settings help ensure the presets display as designed and make it easy to recall your preferred look.

Global configuration options

The distribution includes config/options.skel.cfg as a template for optional global shader defines.

To use global options:

  1. Copy config/options.skel.cfg to config/options.cfg.

  2. Uncomment the #define lines you want to enable.

  3. Keep options.skel.cfg unchanged; keep your customizations in options.cfg.

Global options are compile-time toggles, so they affect shader behavior globally rather than per-preset.

Available options in the skeleton include:

  • OPTION_DEBUG: enables debug parameters in shaders

  • OPTION_NOBEZEL: disables bezel and glow rendering

  • OPTION_NOGLOW: disables glow rendering when bezel is enabled

  • OPTION_NOBEZEL_ZOOM <value>: zoom compensation when bezel is disabled (default 1.07)

  • OPTION_NOCOLOR: disables color correction (effectively Rec.709 behavior)

  • OPTION_NOCAT: disables chromatic adaptation

  • OPTION_FLAT: forces flat geometry with no curvature or distortion

  • OPTION_NOSCANLINES: disables blank scanlines

  • OPTION_NOMASK: disables mask effects

  • OPTION_NOPHOSPHOR: disables phosphor decay effects

  • OPTION_CRISPY: disables R/G/B/Y bandwidth filters (and sharpening circuit), while keeping chroma/special filters active

Installing additional presets

Download the latest EXTRAS release from GitHub and copy the files to your installation folder. For RetroArch, this is typically shaders/shaders_slang/bezel/scanline-classic.

Building the Shader Presets

The presets are built dynamically from a Python script, build.py. See external/presetgen for dependency information.

After building, the shaders can be found in the out directory.

Linting shader formatting

To keep .slang and .inc formatting consistent in shaders/, run:

  • python scripts/lint_shaders.py

For stricter structural checks (stage flow, universal declaration placement, and push/UBO structure checks), run:

  • python scripts/lint_shaders.py --strict-structure

For safe automatic cleanup (trailing whitespace, excessive blank lines, EOF newline):

  • python scripts/lint_shaders.py --fix

To gate builds on shader lint, run:

  • python build.py --lint-shaders

To gate builds on strict structural shader lint, run:

  • python build.py --lint-shaders --strict-structure

To enable local pre-commit shader lint in this repo, run once:

  • git config core.hooksPath .githooks

Default lint checks include:

  • Spaces-only policy (tab characters are not allowed)

Strict-mode lint checks (--strict-structure) include:

  • Enforced order in .slang files: parameter pragmas -> push block definition -> push semantic defines -> UBO definition -> UBO semantic defines

  • Push-constant size budgeting against a 128-byte limit

  • Push/UBO optimization warning when push constants are below 128 bytes and non-MVP semantics still live in UBO (evaluated against max possible push size across conditional paths)

  • Stage flow in .slang: exactly 1 vertex section and 1 fragment section, ordered as universal declarations -> vertex -> fragment

  • .inc files must not declare shader stages (#pragma stage vertex|fragment)

  • Universal functions/variables in .slang are expected to be shared by both vertex and fragment; stage-specific declarations are flagged for relocation into their respective section

  • Intentional exceptions can be explicitly marked with // lint: allow-stage-local immediately above a declaration when stage-local placement is required

For day-to-day authoring guidance, see doc/SHADER_AUTHORING_CHECKLIST.md.

Continuous integration runs build-path lint gating via .github/workflows/shader-lint.yml.

CI executes python build.py --lint-shaders --jobs 1 by default, and uses python build.py --lint-shaders --strict-structure --jobs 1 for master pushes and pull requests targeting master.

Usage

Shaders

The shader pipeline is modular and grouped by function (system signal path, encoding/decoding, CRT simulation, output transform, and bezel/glow composition).

See the shaders/ directory for the complete and current pass set.

Parameters

For details about all the parameters available in the shader, see doc/PARAMETERS.md

SDR, WCG, and HDR Shaders

SDR (Standard Dynamic Range) shaders (e.g., *-sdr.slang) are designed for typical displays with standard color gamuts and brightness. WCG (Wide Color Gamut) shaders (e.g., *-wcg.slang) are optimized for displays that support a wider color space and higher dynamic range, providing richer and more accurate color reproduction. HDR (High Dynamic Range) shaders (e.g., *-hdr.slang) target HDR-capable displays and are currently in beta; expect occasional changes and verify results on your own display. Use the WCG or HDR variants if your display supports BT.2020, otherwise use the SDR versions for best compatibility.

Bugs

Report bugs to anikom15.

Credits

Design and Programming

  • W. M. Martinez

Original Mitchell-Netravali Shader Authors

12 Likes

EDIT: Post stolen for parameters file, original below.

Parameters Cheatsheet

Copyright © 2025-2026 W. M. Martinez.

Copying and distribution of this file, with or without modification,

are permitted in any medium without royalty provided the copyright

notice and this notice are preserved. This file is offered as-is,

without any warranty.

A quick reference to common parameters across Scanline Classic presets and shaders. Values shown are typical ranges.

User Settings

Represents controls accessible to an end user.

Common Controls

  • USER_PICTURE - Picture (%): Display ‘Picture’ or ‘Contrast’ control

  • USER_BRIGHTNESS - Brightness (%): Display ‘Brightness’ or ‘Black level’ control

  • USER_SHARPNESS - Sharpness (%): Controls the mixing amount for a twin-peak sharpening circuit

Video Controls

  • USER_COLOR - Color (%): Controls saturation, can be calibrated with blue toggle

  • USER_TINT - Tint (°): Controls tint, can be calibrated with blue toggle; should be set to 0 for PAL

  • USER_MONOCHROME - Monochrome toggle (off, standard, sepia): Disables color for black-and-white presentation; sepia biases the monochrome image toward a warm brown tone

Professional Controls

  • USER_GAMMA - Gamma (0.1): Adjusts the relative gamma by 0.1 increments

  • USER_H_SIZE - Horizontal size (%): Adjusts electronic display width

  • USER_V_SIZE - Vertical size (%): Adjusts electronic display height

  • USER_H_POS - Horizontal position (%): Adjusts horizontal center

  • USER_V_POS - Vertical position (%): Adjusts vertical center

  • USER_BLUE_ONLY - Blue toggle (off, on): Sets the display to output only the blue channel for color calibration

  • USER_UNDERSCAN_TOGGLE - Underscan toggle (off, on): Shrinks the picture, allowing the user to see the overscan region

Output Settings

Controls the presentation of the simulated display on the output display.

Curvature

  • ZOOM - Viewport zoom (%): Zooms or shrinks the curved display

  • SCREEN_FOCUS - Focus (%): Controls the sharpness of the curvature filter; 50% is recommended

Tone Mapping

  • TONEMAP_TYPE - Tone map output (off, Reinhard, Neutral, Hable, ACES): Selects a tone mapping technique; Neutral is a good default, while Hable and ACES are stronger filmic options

  • VIEWING_ENVIRONMENT - Viewing environment (dark, dim, bright): HDR only; selects the scene-adaptation environment used by the tone-mapping stage

Color Correction

  • CHROMATIC_ADAPTATION - Chromatic adaptation (off, LBrad, Z-L, Brad): Selects the technique for translating the source white point to the display white point; Linear Bradford or Zhang-Li is generally recommended

  • ADAPTATION_LEVEL - Adaptation Level (Z-L only): Controls how strongly the Zhang-Li adaptation model is applied; only used when Z-L is selected above

  • GAMUT_COMPRESSION - Gamut mapping (off, basic, Luv, IPT): SDR only; selects the technique for translating out-of-gamut colors to the display gamut; Luv is a good quality default, while off/basic are faster; IPT is an alternative to Luv that better maintains blue hues at the cost of saturation

  • GAMUT_SELECT - Output gamut (BT.2020, DCI-P3): WCG only; selects the target gamut container for wide-color output presets

Bezel

  • VIEWPORT_H_POS - Viewport horizontal position: Adjusts horizontal center of the viewport relative to the final display

  • VIEWPORT_V_POS - Viewport vertical position: Adjusts vertical center of the viewport relative to the final display

  • BEZEL_ZOOM - Bezel zoom: Scales the bezel artwork independently of the viewport; useful when matching a larger or smaller cabinet opening

  • BEZEL_GAIN - Bezel gain (dB): Adjusts brightness of the bezel

  • BEZEL_BIAS - Bezel bias (IRE): Adjusts black level of the bezel

Glow

  • GLOW_WEIGHT - Glow weight: Adjusts the mixing amount of glow upon the bezel

  • USE_MIPMAPPING - Use mipmapping for glow (non-D3D only): Uses mipmap sampling for smoother glow accumulation on supported backends; usually best left on when available

  • GLOW_TEMPERATURE - Glow color temperature: Adjusts bias toward or away from blue; generally, white light appears bluer as it diffuses

  • GLOW_DIFFUSION - Glow diffusion (low, medium, high): Adjusts how diffuse the glow sampling will be; increasing it impacts performance

  • GLOW_RADIUS_PERCENT - Glow radius: Affects radius of sample steps for the glow

  • GLOW_COMPRESSION - Glow saturation: Lowering this value reduces the saturation of the glow, simulating how light becomes white as it mixes

  • GLOW_DITHER - Glow dither (dB): Adds dithering to large glow gradients to reduce banding; stronger dither can be helpful on low-bit-depth output paths

  • GLOW_FALLOFF - Glow falloff: Increasing this value causes the glow to darken more rapidly as it spreads away from the viewport

  • EDGE_GUARD_PERCENT - Glow edge margin: Reserves space near the viewport edge so the glow does not clip or sample too aggressively at the border

Service Settings

Represents controls available either through the service menu, from the back of the unit, or otherwise practically possible for a technician to adjust.

Geometry

  • UNDERSCAN - Underscan (%): The amount of extra space to display when the underscan mode is enabled

  • BEAM_FOCUS - Beam focus (%): Adjusts sharpness of the beam spot

  • S_CORRECTION_H - Horizontal S-correction (%): Electronic polynomial geometry correction

  • S_CORRECTION_V - Vertical S-correction (%): Electronic polynomial geometry correction

  • TRAPEZOIDAL_CORRECTION - Trapezoidal correction (%): Can be used to correct for pincushion, relative to the electronic picture

  • CORNER_CORRECTION - Corner correction (%): Tucks in the corners without affecting other parts of the screen

  • MAGNETIC_CORRECTION - Magnetic correction (%): Used to correct for pincushion, relative to the physical screen

Scan Raster

  • DOUBLE_REFRESH - Double refresh rate (eliminates flicker): Some PAL displays worked at 100 Hz instead of 50 Hz. This simulates that effect by eliminating flicker; makes use of subframes when available

  • SCANLINE_BLANK_WEIGHT - Blank scanline weight (%): Controls how much blank scanlines are darkened. Simulates how different beam spot sizes and TVL can affect blank scanline visibility

S-Video Input

  • DECODER_SETUP - Setup (off, on): When enabled, the decoder will pull down the luminance level by 7.5 IRE and then normalize gain after processing

  • DECODER_TYPE - Decoder type (NTSC, PAL): Selects the decoder standard for the incoming Y/C signal; use PAL only when the source actually uses PAL encoding

  • BW_MODE - Monitor mode (off, BW, Y, C, Y+C): Used for calibration; the technician connects the S-Video cable to a breakout and remixes: BW displays a composite-like sum, Y displays the luminance only, C displays the chroma, Y+C displays both as separate channels

  • DISPLAY_BANDWIDTH_Y - Display bandwidth Y (MHz): Frequency response of the luminance channel in the display decoder path

  • DISPLAY_BANDWIDTH_C - Display bandwidth C (MHz): Frequency response of the chroma channel in the display decoder path

  • DISPLAY_CUTOFF_ATTEN_Y - Display cutoff attenuation Y (dB): Cutoff slope strength for the luminance channel

  • DISPLAY_CUTOFF_ATTEN_C - Display cutoff attenuation C (dB): Cutoff slope strength for the chroma channel

Composite Input

  • DECODER_SETUP - Setup (off, on): See S-Video Input

  • DECODER_TYPE - Decoder type (NTSC, PAL): Selects the composite decoder standard; PAL enables the PAL line decoder path

  • PAL_DECODER_MODE - PAL Decoder Mode (simple, delay-line, adaptive): Selects the PAL line decoding method; adaptive is usually the most robust, while simple can better resemble low-cost sets

  • BW_MODE - Monitor mode (off, BW, Y, C, Y+C): Practically the same as the S-Video setting, but represents feeding off the encoder hardware directly

  • COLOR_FILTER_MODE - Filter mode (notch, simple comb, adaptive comb): Selects the composite separation filter technique; notch is usually best for 2D content, adaptive comb is often better for 3D or detailed motion

  • NOTCH_WIDTH - Luma notch filter width (MHz): Increasing this value reduces color bleeding at the expense of sharpness

  • NOTCH_ATTEN_DB - Luma notch filter attenuation (dB): Increasing this value reduces color bleeding at the expense of sharpness

  • BANDPASS_WIDTH - Chroma bandpass width (MHz): Increasing this value improves color resolution at the expense of introducing artifacts

  • BANDPASS_ATTEN_DB - Chroma bandpass attenuation (dB): Decreasing this value improves color resolution at the expense of introducing artifacts

  • COMB_FILTER_LUMA_ADAPT - Comb filter luma correlation factor: Affects the threshold for determining whether to engage the comb filter on luma when the adaptive comb filter is used; higher values require a stronger correlation between lines

  • COMB_FILTER_2D - Comb filter type (1D, 2D): When 2D is used, the comb filter will analyze the output result against the input for luma restoration; recommended to leave this on 2D

  • DISPLAY_BANDWIDTH_Y - Display bandwidth Y (MHz): See S-Video Input

  • DISPLAY_BANDWIDTH_C - Display bandwidth C (MHz): See S-Video Input

  • DISPLAY_CUTOFF_ATTEN_Y - Display cutoff attenuation Y (dB): See S-Video Input

  • DISPLAY_CUTOFF_ATTEN_C - Display cutoff attenuation C (dB): See S-Video Input

RGB

These controls affect the translation of color to the electron guns.

  • DISPLAY_BIAS_R, DISPLAY_BIAS_G, DISPLAY_BIAS_B - Bias R/G/B (IRE): Adjust gun black level for each channel

  • DISPLAY_GAIN_R, DISPLAY_GAIN_G, DISPLAY_GAIN_B - Drive R/G/B (dB): Adjust gun gain for each channel

  • DISPLAY_BANDLIMIT_R, DISPLAY_BANDLIMIT_G, DISPLAY_BANDLIMIT_B - Bandwidth R/G/B (MHz): Adjust high-frequency response of the R/G/B channels

  • DISPLAY_CUTOFF_ATTEN_R, DISPLAY_CUTOFF_ATTEN_G, DISPLAY_CUTOFF_ATTEN_B - Cutoff attenuation R/G/B (dB): Adjust filter roll-off strength for the R/G/B channels

Factory Settings

These settings represent design parameters that are fixed to the display and cannot be changed after manufacturing.

Geometry

  • ASPECT - Display aspect ratio (4:3, 16:9, 5:4, 16:10): Selects the aspect ratio of the simulated display. Content is adjusted to fit within the frame without distortion; this setting does not affect the content presentation aspect ratio

  • DEFLECTION_ANGLE - Deflection angle (°): Represents the CRT deflection geometry. Higher angles generally imply a shallower tube and stronger geometric demands on the raster

  • SCREEN_ANGLE_H - Screen angle H (°): The horizontal screen angle of curvature, typically a low angle of no more than 45°

  • SCREEN_ANGLE_V - Screen angle V (°): The vertical screen angle of curvature; use 0 for Trinitron displays

Shadow Mask

  • TVL - Horizontal phosphor triad count: Effective horizontal phosphor density of the display

  • MASK_TYPE - Mask type (aperture, slot, shadow): Selects the mask layout; aperture is brightest, slot has medium brightness, shadow is darkest

  • MASK_INTENSITY - Mask intensity (%): Adjusts the strength of the mask effect; recommended to leave at 100 unless brightness is limited

Colorimetry

  • COLORIMETRY_PRESET - Colorimetry preset (off, SMPTE, Japan, EBU, 709): Presets colorimetry to the given standard

  • R_X, R_Y - Phosphor red x/y: Selects the chromaticity coordinate for the red primary when a preset is not used

  • G_X, G_Y - Phosphor green x/y: Selects the chromaticity coordinate for the green primary when a preset is not used

  • B_X, B_Y - Phosphor blue x/y: Selects the chromaticity coordinate for the blue primary when a preset is not used

  • W_X, W_Y - White point x/y: Selects the white point chromaticity coordinate

Phosphors

  • PHOSPHOR_MANTISSA_R, PHOSPHOR_MANTISSA_G, PHOSPHOR_MANTISSA_B - Phosphor decay mantissa R/G/B (s): Selects the phosphor decay mantissa where decay time is mantissa * 10 ^ exponent seconds

  • PHOSPHOR_EXPONENT_R, PHOSPHOR_EXPONENT_G, PHOSPHOR_EXPONENT_B - Phosphor decay exponent R/G/B (base-10): Selects the phosphor decay exponent where decay time is mantissa * 10 ^ exponent seconds

  • PHOSPHOR_HOLD_R, PHOSPHOR_HOLD_G, PHOSPHOR_HOLD_B - Phosphor tail hold R/G/B (order): Adjusts tail strength of the phosphor decay. Higher values result in a longer overall decay

Limiter

  • NTSC_CONVERSION - NTSC conversion matrix (off, on): Enables a conversion matrix to approximate 1953 NTSC colors with limited phosphors; approximates non-standard color decoding on consumer-grade televisions

  • NTSC_CONVERSION_WEIGHT - NTSC conversion weight: The level of approximation the display should attempt to correct for when using the NTSC conversion matrix

  • LIMITER_TYPE - Limiter type (soft, hard): Chooses a soft or hard knee for signals that go beyond the compression point, after input gains and biases but before user controls

  • LIMITER_COMPRESSION_POINT - Compression point (IRE): The IRE level at which the compression knee will begin to be applied. When the hard knee is enabled, this becomes the maximum allowed output level

  • LIMITER_MAX_OUTPUT - Maximum output (IRE): The absolute IRE level that is allowed when the soft knee is used. Higher levels will be clipped

RF Input

  • IQ_DEMOD_Q_WEIGHT - Vestigial weight: Has a minor effect on restoring luminance after RF demodulation

System Settings

These settings apply to the system, i.e. everything that happens before the signal gets to the simulated display.

RF Modulator

  • USB_BANDWIDTH - Video channel bandwidth (MHz): The upper sideband bandwidth of the RF signal

  • LSB_BANDWIDTH - Vestigial bandwidth (MHz): The lower sideband bandwidth of the RF signal

  • USB_ROLL_OFF - Video channel roll-off (MHz): How much transition is allowed between the passband and stop band for the upper sideband RF signal

  • LSB_ROLL_OFF - Vestigial roll-off (MHz): How much transition is allowed between the passband and stop band for the lower sideband RF signal; usually 0.75 MHz

  • NOISE_MODE - Noise mode (fast, advanced): Fast is AWGN only; advanced has more realistic effects at the expense of performance

  • NOISE_AWGN_DB - AWGN level (dB): Determines the overall noise level of the RF signal

  • NOISE_PHASE_JITTER_DEG - Phase jitter (° RMS): The amount of phase jitter introduced to the RF signal

  • NOISE_GHOST1_DB - Ghost1 level (dB): Controls visibility of multipath ghost; for direct connections, a ghost is unlikely

  • NOISE_GHOST1_PIX - Ghost1 delay (px): Controls the offset of the multipath ghost

  • NOISE_IMPULSE_RATE - Impulse rate (ppm): The frequency of shot noise

  • NOISE_IMPULSE_DB - Impulse level (dB): The level for each shot-noise impulse

Timing

  • SC_FREQ_MODE - Subcarrier frequency mode (auto, NTSC, PAL, PAL-M, custom): Auto mode attempts to determine the standard from core refresh rate; NTSC, PAL, and PAL-M are fixed to standards, and custom is determined by SC_FREQ

  • SC_FREQ - Custom subcarrier frequency (MHz): See SC_FREQ_MODE

  • PIXEL_CLOCK_MODE - Pixel clock mode (fixed, multiple of subcarrier): Selects whether to treat PIXEL_CLOCK as an absolute value or as a multiple of the subcarrier frequency

  • PIXEL_CLOCK - Pixel clock frequency (MHz) / multiplier: Determines the pixel clock according to PIXEL_CLOCK_MODE; affects how horizontal frequency and line count are determined

  • H_FREQ_MODE - Horizontal frequency mode (standard, pixel clock divisor, custom): Selects how to determine horizontal frequency. Standard chooses a horizontal frequency according to the detected video standard regardless of pixel clock, pixel clock divisor divides the pixel clock by H_FREQ, and custom treats H_FREQ as an absolute value

  • H_FREQ - Custom horizontal frequency (kHz) / divisor: Determines the horizontal frequency according to H_FREQ_MODE; affects how vertical lines and vertical frequency are determined

  • V_FREQ_MODE - Vertical sync mode (auto, NTSC, PAL, horizontal frequency divisor, custom): Determines how the vertical sync rate is determined. Auto uses an algorithm based on the reported core refresh to recover exact precision. NTSC and PAL select 59.94 and 50 respectively, horizontal frequency divisor takes the horizontal frequency and divides by V_FREQ, and custom is an absolute value

  • V_FREQ - Custom vertical sync rate (Hz) / divisor: Determines the vertical frequency according to V_FREQ_MODE

  • H_BLANK_FUZZ - Horizontal blanking fuzz factor (%): Sets a factor used to recover the total horizontal pixel timing using fuzzy math. It does not need to be precise

  • FIELD_ORDER - Field order (even first, odd first): Sets which field is considered first

  • SHORTEN_ODD_FIELD_TIME - NES/SNES field time adjust (off, on): Adjusts signal timing according to interlace mode and detected video standard, specifically for the NES and SNES; should be off on any other systems

RGB

These settings are identical in purpose to Display RGB, but apply at the system level earlier in the shader pipeline.

  • SYS_BIAS_R, SYS_BIAS_G, SYS_BIAS_B - Offset R/G/B (IRE): Sets black level for the system RGB channels before display simulation

  • SYS_GAIN_R, SYS_GAIN_G, SYS_GAIN_B - Gain R/G/B (dB): Sets gain for the system RGB channels before display simulation

  • SYS_BANDWIDTH_R, SYS_BANDWIDTH_G, SYS_BANDWIDTH_B - Bandwidth R/G/B (MHz): Controls frequency response of the system RGB channels

  • SYS_CUTOFF_ATTEN_R, SYS_CUTOFF_ATTEN_G, SYS_CUTOFF_ATTEN_B - Cutoff attenuation R/G/B (dB): Controls filter roll-off strength for the system RGB channels

Component Video

  • YC_MODEL - System color space (YPbPr, YDbDr, YCbCr): Selects the RGB-to-component conversion matrix

  • SYS_BIAS_Y, SYS_BIAS_U, SYS_BIAS_V - Offset Y/U/V (IRE): Sets the offset for the given component channel

  • SYS_GAIN_Y, SYS_GAIN_U, SYS_GAIN_V - Gain Y/U/V (dB): Sets the gain for the given component channel

  • SYS_BANDWIDTH_Y, SYS_BANDWIDTH_U, SYS_BANDWIDTH_V - Bandwidth Y/U/V (MHz): Controls frequency response of the Y/C components

  • SYS_CUTOFF_ATTEN_Y, SYS_CUTOFF_ATTEN_U, SYS_CUTOFF_ATTEN_V - Cutoff attenuation Y/U/V (dB): Controls filter roll-off strength of the Y/C components

Encoder

  • ENCODER_SETUP - Setup (off, on): Pushes the luminance level by 7.5 IRE and normalizes gain before encoding

  • PAL - Phase alternating line (off, on): Enables PAL line encoding for phase recovery

Famicom/NES Filter

  • FILTER_BANDWIDTH - Filter bandwidth (MHz): Controls the nominal low-pass bandwidth used by the Famicom/NES composite filter path

  • FILTER_CUTOFF_ATTEN - Filter cutoff attenuation (dB): Controls attenuation strength at the filter cutoff point in the Famicom/NES filter path

  • FILTER_DPD_AMOUNT_NS - Filter DPD amount (ns): Controls differential phase delay amount used to model brightness-dependent hue shift behavior

  • FILTER_DPD_CURVE - Filter DPD curve: Controls how strongly differential phase delay is concentrated into brighter signal regions

  • FILTER_DPD_IN_MIN - Filter DPD input min: Sets the lower input bound of the signal window mapped into DPD drive

  • FILTER_DPD_IN_MAX - Filter DPD input max: Sets the upper input bound of the signal window mapped into DPD drive

Y/C Processing

  • YC_MODEL - System color space (YPbPr, YDbDr, YCbCr): Selects the RGB-to-Y/C conversion matrix used before encoding or modulation

  • SYS_BIAS_Y, SYS_BIAS_U, SYS_BIAS_V - Offset Y/U/V (IRE): Sets offset for the luminance and chroma channels in the Y/C path

  • SYS_GAIN_Y, SYS_GAIN_U, SYS_GAIN_V - Gain Y/U/V (dB): Sets gain for the luminance and chroma channels in the Y/C path

  • LUMA_LOWPASS_CUTOFF - Y lowpass cutoff (MHz): Controls luma lowpass frequency response

  • LUMA_LOWPASS_ATTEN - Y lowpass attenuation (dB): Controls the amount of attenuation at the lowpass cutoff

  • LUMA_NOTCH_ENABLE - Y notch filter (off, on): Enables a system-level notch filter to reduce color bleeding at the expense of sharpness

  • LUMA_NOTCH_WIDTH - Y notch filter width (MHz): Controls notch filter width

  • LUMA_NOTCH_ATTEN_DB - Y notch filter attenuation (dB): Controls notch filter attenuation

  • CHROMA_BANDPASS_WIDTH - Chroma bandpass width (MHz): Controls chroma separation frequency response; unlike the notch, this is strictly necessary for correct encoding

  • CHROMA_EDGE_ATTEN_DB - Chroma edge attenuation (dB): Controls how strongly the chroma bandpass tapers near its edge; increasing it can suppress artifacts at the expense of color detail

Digital Video

  • WORKING_BITS - Working bit depth: Sets internal digital precision before later analog-style stages are applied; higher values reduce quantization error

  • YCBCR_MODEL - System color space (601, 709): Selects the digital YCbCr conversion matrix for the source path

  • DIGITAL_Y_FILTER_DECIMATION - Y bandwidth decimation: Reduces luminance bandwidth in the digital domain; lower effective bandwidth simulates softer filtering

  • DIGITAL_Cb_FILTER_DECIMATION - Cb bandwidth decimation: Reduces Cb chroma bandwidth in the digital domain

  • DIGITAL_Cr_FILTER_DECIMATION - Cr bandwidth decimation: Reduces Cr chroma bandwidth in the digital domain

  • DAC_BITS - Output bit depth: Sets the precision of the simulated digital-to-analog converter; lower values make quantization artifacts easier to see

  • DITHER - Dithering (off, on): Applies dithering before the DAC stage to reduce visible banding and quantization steps

  • VIDEO_MODE - Output mode (Composite, S-Video, Component): Selects which analog output path the digital encoder feeds

  • SYS_BIAS_Y, SYS_BIAS_U, SYS_BIAS_V - Offset Y/C/Pb/Pr (IRE): Sets output offsets for the simulated DAC path; exact channel meaning depends on VIDEO_MODE

  • SYS_GAIN_Y, SYS_GAIN_U, SYS_GAIN_V - Gain Y/C/Pb/Pr (dB): Sets output gain for the simulated DAC path; exact channel meaning depends on VIDEO_MODE

Chroma Subsampling

  • CHROMA_SUBSAMPLE - Subsampling mode (auto, 4:4:4, 4:2:2, 4:2:0): Selects digital chroma subsampling format before later processing; lower chroma resolution can better match consumer digital video paths

Digital Upsampler

  • UPSAMPLER_INPUT - Upsampler input type (RGB, YCbCr): Selects the signal domain fed into the digital upsampler

  • GAMMA_CORRECT_BLENDING - RGB: gamma correct blending (off, fast, accurate): Controls whether RGB upsampling blends in gamma-corrected space; accurate is best quality, fast is cheaper

Global Options

These are compile-time options from config/options.skel.cfg. Unlike runtime parameters above, they are enabled by editing config/options.cfg.

  • OPTION_DEBUG - Enable debug parameters in shaders

  • OPTION_NOBEZEL - Disable bezel and glow rendering

  • OPTION_NOGLOW - Disable glow rendering when bezel rendering is enabled

  • OPTION_NOBEZEL_ZOOM - Apply additional zoom when bezel is disabled to compensate for bezel crop; default is 1.07 (7%)

  • OPTION_NOCOLOR - Disable color correction and treat the pipeline effectively as Rec.709

  • OPTION_NOCAT - Disable chromatic adaptation (for example, NTSC-J presets will render at 9300K relative to D65)

  • OPTION_FLAT - Force flat geometry with no curvature or geometric distortion

  • OPTION_NOSCANLINES - Disable blank scanline rendering

  • OPTION_NOMASK - Disable mask effects

  • OPTION_NOPHOSPHOR - Disable phosphor decay effects

  • OPTION_CRISPY - Disable R/G/B/Y bandwidth filters for the sharpest pixel response (does not disable chroma filters or special filters such as the Famicom filter; also disables sharpening)


Updates on what I’ve been working on

I want a shader that can mimic the functionality of Multi-Scan CRTs. It’s a myth that CRTs have no native resolution or can handle any display mode thrown at them, otherwise we would have never needed circuitry to handle NTSC signals on PAL televisions! While CRTs are a lot more flexible than LCDs in how they present a signal, they are in many ways a lot less flexibile and scaleable than LCDs because they usually only operate on a fixed horizontal frequency. For decades that was about 15 kHz, but that started to change in the 16-bit computing era.

One of the pioneers for faster scanning displays was Atari, who used ‘medium res’ monitors in some of their games. The idea behind medium res is simple, take the resolution of a PAL monitor and combine it with the refresh rate of an NTSC monitor. Voila, you have a monitor that can display more lines without the flicker.

If you see a computer system or arcade game that runs at around 384 lines, you know it’s a medium res game. Mac 68ks and the IBM EGA monitor used these resolutions.

Multitasking started to become more important in the mid-80s. 240 and 384 line displays were adequate for single-task workflows, but felt cramped in multi-tasking environments. The easiest way to experience this is to emulate something like a Mac SE/30 and exploring the file system with Finder. Your screen will fill up with windows very quickly. 480 line displays were sorely needed. The Japanese had already solved this problem with 480i modes (because their language requires more pixel-space to render clearly), but this was improved upon by doubling the 15 kHz scan rate to 31 kHz, and thus 480p monitors came about.

But what if we want to display a 15 kHz signal on a 31 kHz monitor? Now the issue of being able to drive multiple signal types with a CRT really needed to be solved. It couldn’t simply be ignored because we needed backwards compatibility with old software designed for ~240 line environment. Video cards also supported more colors on the lower resolutions, which were critical for games. The Japanese (again) had already solved this problem. When a 31 kHz monitor encounters a 15 kHz signal, it doubles the lines and displays the signal with the vertical resolution essentially upscaled as NN. But these monitors were quite expensive. IBM came up with the solution of doing the line-doubling with the video card, so any 31 kHz monitor can be used with the ‘15 kHz’ signals.

Finally it was in the 90s where Multi-Scan capable monitors started to become more common. A CRT monitor would be advertised as supporting a maximum resolution, and the CRT would have built-in circuitry to handle most of the common modes. The CRT could theoretically handle a much larger variety of modes as long as it fell within the configurable horizontal frequency range. Over time, the minimum frequency increased until it capped to 31 kHz, VGA frequency, because VGA compatibility was so critical.

Why does any of this matter for shaders? It doesn’t matter at all for consoles (at least not until we get into HD consoles, at which point CRTs were so rare it’s perhaps not meaningful to have CRT shaders for them). It only matters for computer emulation, and only if we care about the artifacts of a multi-scan monitor. There are three primary complications that multi-scanning has: 1. Scanlines become more visible as lower resolutions are used; 2. the overall brightness of the screen diminishes at lower resolutions (because of 1); and 3. the geometry and positioning of the screen changes depending on the resolution chosen.

1 and 2 are easy enough to implement. We just have to decide on what the ideal resolution of our simulated monitor is. That resolution would not have visible scanlines. When the emulator switches to a mode with a smaller resolution, we would add scanlines. Adding the scanlines would automatically cover 2. 3 is complicated, and every monitor would handle these differently. Actually, it goes back to number 1, because they wouldn’t necessarily cover the same picture area with a lower resolution, and so scanlines might not scale linearly in that respect. Another complication is aspect ratio. Most monitors would just spit everything out in whatever aspect ratio they were designed for, so a 4:3 monitor would distort a 5:4 resolution and vice versa. Late professional monitors (very expensive) could actually handle different aspect ratios and display them correctly.

But 3 is really a non-issue when you consider that by the time Multi-Scan monitors were common, geometry controls on monitors were also common, and usually the user would adjust the picture to fill out the screen to a comfortable (usually we would leave a bit of space to avoid too much curvature). So really it is just 1 that I’m going to focus on.

6 Likes

FYI the Multi-Scan feature has been added and the shader is now at version 4.1. At this point I feel like the shader has all the features needed, but I’ll play with it for a few months to see if I can think of anything else to add.

As a bonus here’s a screenshot of DOS in 80x25 mode on a simulated 1024x768 monitor:

8 Likes

Hey there anikom15,

I read your comment about contrast simulation of a CRT in this other thread (https://forums.libretro.com/t/old-monitor-style-shader-and-overlay-for-dosbox-pure/41535/26 ). That’s definitely an interesting topic. Since you seem to value gamma quality I thought you may find the following about contrast simulation useful.

For simulating the analog CRT “brightness” (black level in analog terms) and “contrast” (brightness in analog terms) of old monitors and TV’s implementing BT. 1886 gamma control seems to do exactly what you want regarding analog contrast, without faking it. Note that Appendix 1 and 2 of the official spec document is what you would want to look into, not the vanilla implementation.

I’ve written a bit about it in the past in another thread and separately from this recently Dogway has implemented it in his grade - update 2023.slang shader.

My post on it here: SpectraCal’s Director of Software Development, Joel Barsotti, argues that the BT. 1886 gamma function (EOTF) more accurately matches legacy CRT technology than previous power law based functions. The argument is that it properly takes into account the legacy “contrast” and “brightness” controls on CRTs.

SpectraCal FAQ on BT. 1886 here: BT.1886 – 10 Questions, 10 Answers

The relevant Rec. ITU-R BT. 1886 document here: Recommendation ITU-R BT.1886 . Be sure to read Appendix 1 and Appendix 2 as that is what you would want to play around with.

Not that you need it, but if you want a quick look at the effect of implementing these analog controls through BT. 1886 EOTF you could try Dogway’s GRADE shader. Here’s his recent implementation forum post: I updated Grade with what I’ve learned in the last 2 years. And his GitHub with his latest RC6 release of the Grade shader: grade - update 2023 RC6.slang

4 Likes

FYI, a bunch of monochrome presets are now available in the repo.

@rafan, thanks for pointing this out. I’m not sure if I want to add this complexity at this point, but I think it will end up being something that gets added to the advanced shader later down the road.

3 Likes

Well, porting this to MAME is on halt because I can’t seem to compile bgfx-tools and I haven’t been able to get in touch with the guy who maintains that. I probably didn’t set up my environment correctly.

I think I will add another parameter which will break compatibility again. I think having a MIN_SCAN_RATE parameter to finely control the line doubler is justified.

An update! But nothing new to try, just plans for the future. Life moves on and unfortunately I do have a fulltime job and lifestyle I have to keep up. But I digress.

After almost a year of using this shader, I am very happy with it. I never got it ported to MAME’s BGFX driver, but I did create a single pass version that can work as a standalone GLSL shader. I’ll provide that as an update sometime soon. This can be used in many emulators that Retroarch doesn’t support, like 86Box.

The big new thing I’m going to work on this year though will be support for colorspaces other than sRGB! My TV supports 10- and 12-bit input and I just got a video card that can support it. My TV also can accept input signals for DCI, Adobe RGB, and BT.2020 (we’ll see if I can find out how much coverage of each color space the TV supports). Here’s a quick overview of what all of that means:

Adobe RGB is the oldest and was originally used by Photoshop to provide a bigger colorspace for photographers to edit their work in. Adobe RGB is mainly supported in expensive professional monitors. It has largely been supplanted by other spaces.

DCI is the color space used by digital cinema equipment. It is famously used by Apple with some slight variations in their iPhone (and maybe some Mac OS X support?). This could be an important space to support for Retroarch on mobile.

BT.2020 is the largest and most recent of the color spaces. It’s really meant to cover as much of our visible color spectrum as possible in order to display content made in either sRGB, DCI, or something else. This will be the one I work on first as this is the current standard for UHD and HDR content.

10- and 12-bit color is also important. sRGB uses 8-bit color, so if you try to use a larger gamut with just 8 bits you’ll get banding. How visible that is is a matter of debate, but 10-bit and 12-bit color provide enough dynamic range to present any of the above color spaces without banding. My display and video card supports both modes and I will test both.

My TV also supports automatically switching colorspace depending on the content providing, but I’m not sure how that will work with Windows and Retroarch. For now, I will have to manually set the colorspace through the shader and through the TV to match. The big question is if Retroarch will support a 10-bit or 12-bit framebuffer. If it doesn’t, I’ll have to modify it to work. This might be tricky to test!

3 Likes

Version 5.0 is ready. This release includes support for other color spaces. You can see the differences in the screenshots below which were made by using an iPhone and a manual camera app. The white balance was fixed to 6500 K with an ISO of 250 and a shutter speed of 120. Obviously not ideal fidelity, but good enough to highlight the limitations of sRGB in the greens and reds. RTINGS.com states that my TV has a coverage of 88% DCI and 64% BT.2020, 100% sRGB and Adobe RGB was not measured. The difference is subtle, however. To my eye, DCI isn’t a good choice, but this may be due to limitations of my TV. DCI overall looks less saturated. BT.2020 and Adobe RGB look good.

It seems 8-bit color is good enough for the Super Nintendo. Banding may become apparent with something higher fidelity, but for now higher-bit color support will be deprioritized in favor of Scanline LCD (which is a misnomer, but I want to keep the brand consistent). Scanline LCD will be a comprehensive shader for LCD displays, including monochrome, reflective, front-lit, back-lit, etc.

More screenshots of the color space modes can be found here: https://postimg.cc/gallery/cN54H3s

5 Likes

Wow! Great photos! These look almost like GPU Screenshots!

I couldn’t find version 5 on Github.

Really nice shader. I searched a long time for a good one to use for PC and Commodore Emulation. For some reason the 86Box Emulator only uses glsl files. For that I have combined your scanline-basic.glsl shader with the curvature of Hyllian’s. Now it is perfect for me to use with 86Box and on my Rpi!

scanline-basic-curvature.glsl
/* Filename: scanline-basic-curvature.glsl

Red Queen: I modified scanline-basic.glsl shader to include Hyllian's screen curvature!

   Copyright (C) 2010 Team XBMC
   Copyright (C) 2011 Stefanos A.
   Copyright (C) 2023 W. M. Martinez
   Copyright (C) 2025 Red Queen

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>. */

const float EPS = 1.19209289551e-7;
const float PI = 3.14159265359;
const vec3 BLACK = vec3(0.0, 0.0, 0.0);
const vec3 WHITE = vec3(1.0, 1.0, 1.0);
const vec3 RED = vec3(1.0, 0.0, 0.0);
const vec3 YELLOW = vec3(1.0, 1.0, 0.0);
const vec3 GREEN = vec3(0.0, 1.0, 0.0);
const vec3 BLUE = vec3(0.0, 1.0, 1.0);
const vec3 INDIGO = vec3(0.0, 0.0, 1.0);
const vec3 VIOLET = vec3(1.0, 0.0, 1.0);

float sq(const float x)
{
	return x * x;
}

float cb(const float x)
{
	return x * x * x;
}

float crt_linear(const float x)
{
	return pow(x, 2.4);
}

vec3 crt_linear(const vec3 x)
{
	return vec3(crt_linear(x.r), crt_linear(x.g), crt_linear(x.b));
}

float crt_gamma(const float x)
{
	return pow(x, 1.0 / 2.4);
}

vec3 crt_gamma(const vec3 x)
{
	return vec3(crt_gamma(x.r), crt_gamma(x.g), crt_gamma(x.b));
}

float sdr_linear(const float x)
{
	return x < 0.081 ? x / 4.5 : pow((x + 0.099) / 1.099, 1.0 / 0.45);
}

vec3 sdr_linear(const vec3 x)
{
	return vec3(sdr_linear(x.r), sdr_linear(x.g), sdr_linear(x.b));
}

float sdr_gamma(const float x)
{
	return x < 0.018 ? 4.5 * x : 1.099 * pow(x, 0.45) - 0.099;
}

vec3 sdr_gamma(const vec3 x)
{
	return vec3(sdr_gamma(x.r), sdr_gamma(x.g), sdr_gamma(x.b));
}

float srgb_linear(const float x)
{
	return x <= 0.04045 ? x / 12.92 : pow((x + 0.055) / 1.055, 2.4);
}

vec3 srgb_linear(const vec3 x)
{
	return vec3(srgb_linear(x.r), srgb_linear(x.g), srgb_linear(x.b));
}

float srgb_gamma(const float x)
{
	return x <= 0.0031308 ? 12.92 * x : 1.055 * pow(x, 1.0 / 2.4) - 0.055;
}

vec3 srgb_gamma(const vec3 x)
{
	return vec3(srgb_gamma(x.r), srgb_gamma(x.g), srgb_gamma(x.b));
}

mat3 XYZ_TO_sRGB = mat3(
	 3.2406255, -0.9689307,  0.0557101,
	-1.5372080,  1.8758561, -0.2040211,
	-0.4986286,  0.0415175,  1.0569959);

mat3 colorspace_rgb()
{
	return XYZ_TO_sRGB;
}

vec3 RGB_to_xyY(const float x, const float y, const vec3 Y, const vec3 RGB)
{
	return vec3(x, y, dot(Y, RGB));
}

vec3 xyY_to_XYZ(const vec3 xyY)
{
	float x = xyY.x;
	float y = xyY.y;
	float Y = xyY.z;
	float z = 1.0 - x - y;

	return vec3(Y * x / y, Y, Y * z / y);
}

float kernel(const float x, const float B, const float C)
{
	float dx = abs(x);

	if (dx < 1.0) {
		float P1 = 2.0 - 3.0 / 2.0 * B - C;
		float P2 = -3.0 + 2.0 * B + C;
		float P3 = 1.0 - 1.0 / 3.0 * B;

		return P1 * cb(dx) + P2 * sq(dx) + P3;
	} else if ((dx >= 1.0) && (dx < 2.0)) {
		float P1 = -1.0 / 6.0 * B - C;
		float P2 = B + 5.0 * C;
		float P3 = -2.0 * B - 8.0 * C;
		float P4 = 4.0 / 3.0 * B + 4.0 * C;

		return P1 * cb(dx) + P2 * sq(dx) + P3 * dx + P4;
	} else
		return 0.0;
}

vec4 kernel4(const float x, const float B, const float C)
{
	return vec4(kernel(x - 2.0, B, C),
	            kernel(x - 1.0, B, C),
	            kernel(x, B, C),
	            kernel(x + 1.0, B, C));
}

#pragma parameter TRANSFER_FUNCTION "Transfer function" 1.0 1.0 2.0 1.0 
#pragma parameter COLOR_MODE "Chromaticity mode" 3.0 1.0 3.0 1.0
#pragma parameter LUMINANCE_WEIGHT_R "Red channel luminance weight" 0.2124 0.0 1.0 0.01
#pragma parameter LUMINANCE_WEIGHT_G "Green channel luminance weight" 0.7011 0.0 1.0 0.01
#pragma parameter LUMINANCE_WEIGHT_B "Blue channel luminance weight" 0.0866 0.0 1.0 0.01 
#pragma parameter CHROMA_A_X "Chromaticity A x" 0.630 0.0 1.0 0.001
#pragma parameter CHROMA_A_Y "Chromaticity A y" 0.340 0.0 1.0 0.001
#pragma parameter CHROMA_B_X "Chromaticity B x" 0.310 0.0 1.0 0.001
#pragma parameter CHROMA_B_Y "Chromaticity B y" 0.595 0.0 1.0 0.001
#pragma parameter CHROMA_C_X "Chromaticity C x" 0.155 0.0 1.0 0.001
#pragma parameter CHROMA_C_Y "Chromaticity C y" 0.070 0.0 1.0 0.001
#pragma parameter CHROMA_A_WEIGHT "Chromaticity A luminance weight" 0.2124 0.0 1.0 0.01
#pragma parameter CHROMA_B_WEIGHT "Chromaticity B luminance weight" 0.7011 0.0 1.0 0.01
#pragma parameter CHROMA_C_WEIGHT "Chromaticity C luminance weight" 0.0866 0.0 1.0 0.01
#pragma parameter SCALE_W "Scale white point" 1.0 0.0 1.0 1.0 
#pragma parameter SCAN_TYPE "Scan type" 1.0 1.0 2.0 1.0
#pragma parameter MAX_SCAN_RATE "Maximum active lines" 480.0 1.0 1200.0 1.0
#pragma parameter LINE_DOUBLER "Enable line-doubler" 0.0 0.0 1.0 1.0
#pragma parameter INTER_OFF "Interlace offset" 0.0 0.0 1.0 1.0
#pragma parameter FOCUS "Focus (%)" 0.50 0.0 1.0 0.01
#pragma parameter ZOOM "Viewport zoom" 1.0 0.0 10.0 0.01
#pragma parameter COLOR_SPACE "Output color space" 1.0 1.0 4.0 1.0
#pragma parameter CRT_CURVATURE "CRT-Curvature" 1.0 0.0 1.0 1.0
#pragma parameter CRT_warpX "CRT-Curvature X-Axis" 0.031 0.0 0.125 0.01
#pragma parameter CRT_warpY "CRT-Curvature Y-Axis" 0.041 0.0 0.125 0.01
#pragma parameter CRT_cornersize "CRT-Corner Size" 0.01 0.001 1.0 0.005
#define cornersize CRT_cornersize
#pragma parameter CRT_cornersmooth "CRT-Corner Smoothness" 1000.0 80.0 2000.0 100.0
#define cornersmooth CRT_cornersmooth

#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

#ifdef GL_ES
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

COMPAT_ATTRIBUTE vec4 VertexCoord;
COMPAT_ATTRIBUTE vec4 COLOR;
COMPAT_ATTRIBUTE vec4 TexCoord;
COMPAT_VARYING vec4 COL0;
COMPAT_VARYING vec4 TEX0;

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;

#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize)
#define OriginalSize vec4(InputSize, 1.0 / InputSize)
#define OutputSize vec4(OutputSize, 1.0 / OutputSize)

void main()
{
	gl_Position = MVPMatrix * VertexCoord;
	TEX0.xy = TexCoord.xy;
}

#elif defined(FRAGMENT)

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

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

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;
uniform sampler2D Texture;
COMPAT_VARYING vec4 TEX0;

#define Source Texture
#define vTexCoord TEX0.xy

#define SourceSize vec4(TextureSize, 1.0 / TextureSize)
#define OriginalSize vec4(InputSize, 1.0 / InputSize)
#define OutputSize vec4(OutputSize, 1.0 / OutputSize)

#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float TRANSFER_FUNCTION;
uniform COMPAT_PRECISION float COLOR_MODE;
uniform COMPAT_PRECISION float LUMINANCE_WEIGHT_R;
uniform COMPAT_PRECISION float LUMINANCE_WEIGHT_G;
uniform COMPAT_PRECISION float LUMINANCE_WEIGHT_B;
uniform COMPAT_PRECISION float CHROMA_A_X;
uniform COMPAT_PRECISION float CHROMA_A_Y;
uniform COMPAT_PRECISION float CHROMA_B_X;
uniform COMPAT_PRECISION float CHROMA_B_Y;
uniform COMPAT_PRECISION float CHROMA_C_X;
uniform COMPAT_PRECISION float CHROMA_C_Y;
uniform COMPAT_PRECISION float CHROMA_A_WEIGHT;
uniform COMPAT_PRECISION float CHROMA_B_WEIGHT;
uniform COMPAT_PRECISION float CHROMA_C_WEIGHT;
uniform COMPAT_PRECISION float SCALE_W;
uniform COMPAT_PRECISION float SCAN_TYPE;
uniform COMPAT_PRECISION float MAX_SCAN_RATE;
uniform COMPAT_PRECISION float LINE_DOUBLER;
uniform COMPAT_PRECISION float INTER_OFF;
uniform COMPAT_PRECISION float FOCUS;
uniform COMPAT_PRECISION float ZOOM;
uniform COMPAT_PRECISION float COLOR_SPACE;
uniform COMPAT_PRECISION float CRT_CURVATURE;
uniform COMPAT_PRECISION float CRT_warpX;
uniform COMPAT_PRECISION float CRT_warpY;
uniform COMPAT_PRECISION float CRT_cornersize;
uniform COMPAT_PRECISION float CRT_cornersmooth;
#else
#define TRANSFER_FUNCTION 1.0
#define COLOR_MODE 3.0
#define LUMINANCE_WEIGHT_R 0.2124
#define LUMINANCE_WEIGHT_G 0.7011
#define LUMINANCE_WEIGHT_B 0.0866
#define CHROMA_A_X 0.630
#define CHROMA_A_Y 0.340
#define CHROMA_B_X 0.310
#define CHROMA_B_Y 0.595
#define CHROMA_C_X 0.155
#define CHROMA_C_Y 0.070
#define CHROMA_A_WEIGHT 0.2124
#define CHROMA_B_WEIGHT 0.7011
#define CHROMA_C_WEIGHT 0.0866
#define SCALE_W 1.0
#define SCAN_TYPE 1.0
#define MAX_SCAN_RATE 480.0
#define LINE_DOUBLER 0.0
#define INTER_OFF 0.0
#define FOCUS 0.5
#define ZOOM 1.0
#define COLOR_SPACE 1.0
#define CRT_CURVATURE 1.0 
#define CRT_warpX 0.031 
#define CRT_warpY 0.041
#define CRT_cornersize 0.01 
#define CRT_cornersmooth 1000.0
#endif

const vec2 corner_aspect   = vec2(1.0,  0.75);
vec2 CRT_Distortion = vec2(CRT_warpX, CRT_warpY) * 15.;


float corner(vec2 coord)
{
    coord = (coord - vec2(0.5)) + vec2(0.5, 0.5);
    coord = min(coord, vec2(1.0) - coord) * corner_aspect;
    vec2 cdist = vec2(cornersize);
    coord = (cdist - min(coord, cdist));
    float dist = sqrt(dot(coord, coord));
    
    return clamp((cdist.x - dist)*cornersmooth, 0.0, 1.0);
}


vec2 Warp(vec2 texCoord){
  vec2 curvedCoords = texCoord * 2.0 - 1.0;
  float curvedCoordsDistance = sqrt(curvedCoords.x*curvedCoords.x+curvedCoords.y*curvedCoords.y);

  curvedCoords = curvedCoords / curvedCoordsDistance;

  curvedCoords = curvedCoords * (1.0-pow(vec2(1.0-(curvedCoordsDistance/1.4142135623730950488016887242097)),(1.0/(1.0+CRT_Distortion*0.2))));

  curvedCoords = curvedCoords / (1.0-pow(vec2(0.29289321881345247559915563789515),(1.0/(vec2(1.0)+CRT_Distortion*0.2))));

  curvedCoords = curvedCoords * 0.5 + 0.5;
  return curvedCoords;
}

vec3 Yrgb_to_RGB(mat3 toRGB, vec3 W, vec3 Yrgb)
{
	mat3 xyYrgb = mat3(CHROMA_A_X, CHROMA_A_Y, Yrgb.r,
	                   CHROMA_B_X, CHROMA_B_Y, Yrgb.g,
	                   CHROMA_C_X, CHROMA_C_Y, Yrgb.b);
	mat3 XYZrgb = mat3(xyY_to_XYZ(xyYrgb[0]),
	                   xyY_to_XYZ(xyYrgb[1]),
	                   xyY_to_XYZ(xyYrgb[2]));
	mat3 RGBrgb = mat3(toRGB * XYZrgb[0],
	                   toRGB * XYZrgb[1],
	                   toRGB * XYZrgb[2]);
	return vec3(dot(W, vec3(RGBrgb[0].r, RGBrgb[1].r, RGBrgb[2].r)),
	            dot(W, vec3(RGBrgb[0].g, RGBrgb[1].g, RGBrgb[2].g)),
	            dot(W, vec3(RGBrgb[0].b, RGBrgb[1].b, RGBrgb[2].b)));
} 

vec3 color(sampler2D tex, vec2 uv)
{
	vec3 rgb;
	vec3 Yrgb;

	if (TRANSFER_FUNCTION < 2.0)
		rgb = crt_linear(COMPAT_TEXTURE(tex, uv).rgb);
	else
		rgb = srgb_linear(COMPAT_TEXTURE(tex, uv).rgb);
	if (COLOR_MODE < 2.0) {
		vec3 Wrgb = vec3(LUMINANCE_WEIGHT_R,
		                 LUMINANCE_WEIGHT_G,
		                 LUMINANCE_WEIGHT_B);

		Yrgb.r = dot(Wrgb, rgb);
		Yrgb.g = 0.0;
		Yrgb.b = 0.0;
	} else if (COLOR_MODE < 3.0) {
		vec3 Wrgb = vec3(LUMINANCE_WEIGHT_R,
		                 LUMINANCE_WEIGHT_G,
		                 LUMINANCE_WEIGHT_B);
		Yrgb.r = dot(Wrgb, rgb);
		Yrgb.g = Yrgb.r;
		Yrgb.b = 0.0;
	} else {
		Yrgb.r = rgb.r;
		Yrgb.g = rgb.g;
		Yrgb.b = rgb.b;
	}

	mat3 toRGB = colorspace_rgb();
	vec3 W = vec3(CHROMA_A_WEIGHT, CHROMA_B_WEIGHT, CHROMA_C_WEIGHT);
	vec3 RGB = Yrgb_to_RGB(toRGB, W, Yrgb);

	if (SCALE_W > 0.0) {
		vec3 white = Yrgb_to_RGB(toRGB, W, WHITE);
		float G = 1.0 / max(max(white.r, white.g), white.b);

		RGB *= G;
	}
	return clamp(RGB, 0.0, 1.0); 
}

vec3 pixel(float x, float y, sampler2D tex)
{
	float yThres = (SCAN_TYPE < 2.0 && OriginalSize.y > 1.7 * MAX_SCAN_RATE / 2.0)
	               ? OriginalSize.y / 2.0
	               : OriginalSize.y;
	float scanw;
	int line;

	if (LINE_DOUBLER > 0.0 && OriginalSize.y <= MAX_SCAN_RATE / 2.0 + EPS)
		yThres *= 2.0;
	scanw = max(0.0, 2.0 * (yThres / MAX_SCAN_RATE - 0.5));
	if (SCAN_TYPE < 2.0 && OriginalSize.y > 1.7 * MAX_SCAN_RATE / 2.0) {
		int t = FrameCount % 2;
		line = int(2.0 * yThres * y + float(t));
	} else
		line = int(2.0 * yThres * y);
	line += int(INTER_OFF);

	if (any(lessThan(vec2(x, y), vec2(0.0, 0.0))) ||
	    any(greaterThan(vec2(x, y), vec2(1.0, 1.0))))
		return BLACK;
	else {
		if (line % 2 > 0)
			return mix(BLACK, color(tex, vec2(x, y)), scanw);
		else
			return color(tex, vec2(x, y));
	} 
}

vec3 render(float y, vec4 x, vec4 taps, sampler2D tex)
{
	return pixel(x.r, y, tex) * taps.r +
	       pixel(x.g, y, tex) * taps.g +
	       pixel(x.b, y, tex) * taps.b +
	       pixel(x.a, y, tex) * taps.a;
}

void main()
{
	vec2 uv = vTexCoord.xy;

	uv = (CRT_CURVATURE > 0.5) ? (Warp(uv*TextureSize.xy/InputSize.xy)*InputSize.xy/TextureSize.xy) : uv;

	// Normal coordinates
	uv = 2.0 * (uv - 0.5);
	uv /= ZOOM;
	uv = uv / 2.0 + 0.5;

	vec2 stepxy = 1.0 / SourceSize.xy;

	// Scale vertical for scanlines/interlacing
	if (SCAN_TYPE > 1.0 || OriginalSize.y < 1.7 * MAX_SCAN_RATE / 2.0)
		stepxy.y /= 2.0;	

	vec2 pos = uv + stepxy / 2.0;
	vec2 f = fract(pos / stepxy);

	if (LINE_DOUBLER > 0.0 && OriginalSize.y <= MAX_SCAN_RATE / 2.0 + EPS)
		stepxy.y /= 2.0; 

	float C = -1.0 / 3.0 * sq(FOCUS) + 5.0 / 6.0 * FOCUS;
	float B = 1.0 - 2.0 * C;

	vec4 xtaps = kernel4(1.0 - f.x, B, C);
	vec4 ytaps = kernel4(1.0 - f.y, B, C);

	// Make sure all taps added together is exactly 1.0
	xtaps /= xtaps.r + xtaps.g + xtaps.b + xtaps.a;
	ytaps /= ytaps.r + ytaps.g + ytaps.b + ytaps.a;

	vec2 xystart = (-1.5 - f) * stepxy + pos;
	vec4 x = vec4(xystart.x,
	              xystart.x + stepxy.x,
	              xystart.x + stepxy.x * 2.0,
	              xystart.x + stepxy.x * 3.0);

	vec3 col = vec3(render(xystart.y, x, xtaps, Source) * ytaps.r +
	       render(xystart.y + stepxy.y, x, xtaps, Source) * ytaps.g +
	       render(xystart.y + stepxy.y * 2.0, x, xtaps, Source) * ytaps.b +
	       render(xystart.y + stepxy.y * 3.0, x, xtaps, Source) * ytaps.a);

	col = clamp(col, 0.0, 1.0);
	col = crt_gamma(col);

	FragColor = vec4(col, 1.0);

	FragColor *= (CRT_CURVATURE > 0.5) ? corner(uv*TextureSize.xy/InputSize.xy) : 1.0;

}
#endif
1 Like

I have been away from the forum due to getting married and buying a house. But I can tell everyone I still use the shader every time I run RA! If there are any issues, please let me know.

I recently purchased a VRR capable TV, however, the implementation seems janky, and determining if this is caused by the TV, driver, or RA is hard to determine. If you want the absolute smoothest image, using a custom resolution with your display’s BFI mode turned on is probably the best way, especially if you use any of the interlacing features (PSX in particular; many menu screens are interlaced).

If I have time, I will try to get new pictures of the color modes, as this TV has a wider gamut than my old one.

1 Like

I have a big update to share about a new set of shaders, still work in progress. I ended up implementing NTSC/PAL shaders so that you don’t need to use an external one, with the same design philosophy as the original Scanline Classic shaders. The new shaders will require me to reconfigure the options for the old shaders, and I still need to work on an RF modulator mode. I will also need to redo all the presets, and add new presets for different systems.

Doing NTSC shaders the right way is a challenging endeavor, and the shaders have to be tuned for the specific emulator, especially for systems before the 5th generation and 8-bit computers. This is because those old systems did not output a standards-compliant NTSC/PAL signal. That said, I promise to implement as many presets as I can with the information available to get as accurate a simulation as possible.

For simplicity and speed, each major class of video encoding is in its own shader. I have implemented an RGB bandlimit shader, an S-Video shader, and a composite video shader. I will implement an RF shader (very similar to composite). A component video shader may be practically redundant, and I may do a SECAM one for fun, although AFAIK no actual game hardware ever outputted a true color SECAM signal.

Now for screenshots:

RGB

The RGB filter bandlimits the input to the original Scanline Classic shaders (now called the phosphor and advanced shaders) with a Gaussian lowpass filter. The cutoff frequency is adjustable.

S-Video

The S-Video shader converts the source to Y and C and has adjustable Y and C filters.

Composite (Notch Filter)

The notch filter is the simplest filter for composite video. It preserves vertical resolution and lacks ringing, but has poor clarity. My shader implements notch with Gaussian filters to prevent ringing. The filter is always centered on the color subcarrier (either NTSC or PAL can be set), and the left and right cutoff frequencies are individually adjustable.

Composite (Comb Filter)

The screen shots show a single-line comb filter, although two-line and three-line filters are available. The results I got implementing a two-line and three-line were not very pleasant and may need more work. Comb filters provide reduced crosstalk with at the expense of vertical resolution. I have also introduced a setting to blend the notch and comb filter output together. A comb filter for PAL would operate differently, so I still need to account for that.

Composite (Feedback Filter)

This filter is my own idea, based on the principle of differential signaling. I don’t know if it was ever implemented for CRTs (it would require a line to be sampled digitally), but the output looks similar to video recording devices. After an initial demodulation, a chroma signal is regenerated and this new chroma signal is subtracted from the original composite signal to get luminance. The result is a sharp output but with significant crosstalk and some ringing.

The changes are a work in progress but are publicly available in the dev branch. I hope to finalize this work in the next month or so.

5 Likes

Nice! Analog signal simulation for Retroarch is really advancing quickly in the last few months.

After an initial demodulation, a chroma signal is regenerated and this new chroma signal is subtracted from the original composite signal to get luminance

If I understand what you are doing correctly, this is similar to what I’ve seen in some NTSC decoder datasheets and what I’m doing. I demodulate the chroma out of the composite signal, then low-pass filter it, then remodulate it and subtract it from the composite signal to get the luma.

2 Likes

That’s exactly correct. I found that adding more feedback does not seem to improve the result, so one stage is adequate, and it’s fascinating how accurate an image it can restore. I added two debugging features to the shader which could be useful: there is a monochrome setting that lets you see the underlying dot pattern. The shader calculates the dot pattern using assumptions (as parameters) to determine the amount of time spent per pixel per scanline per field. You can also look at the response of the notch’s low pass and high pass components to tune the notch filter.

2 Likes

There was a lot of information missing, intentionally kept secret or not documented at all in all previous ntsc shaders/filters. I asked a dev of such a filter some info about it some years ago and i was getting some foggy answers that were messing things even more. Like you had to keep a massive array of all 256/320 pixels in a scanline and average them and crap like that. Which could be his way of doing anyway.

I think in some cases you could be right too that C has a uniform bandwidth in modern TVs/encoders, that Q lower one could be a thing in some old broadcasts only. Can’t find a clear answer in any datasheet.

2 Likes

Unfortunately as time goes by, much of the old information is falling off the web. My intention is that the code itself works as a way to document the process and keep knowledge of it alive. What’s good is that there is not a single algorithm that needs to be used. The technology was very flexible in the sense that different techniques lead to the same math, or almost the same math, so there are many ways you could implement these types of shaders and be more or less correct.

A big effect on the perceived ‘rainbow colors’ is the actual subpixel output of the console. Thankfully the NES is very well documented and assuming that same timing for the SNES gave me the results here. It may need some adjustments. One thing to keep in mind is that we can see how the comb filter completely changes how the rainbow artifacts appear. Two TVs would not be guaranteed to have the exact same effect. This lines up with my own experience: different TVs of mine handled composite signals differently. Some were horribly messy and others looked nearly imperceptible to S-Video.

In regards to smaller bandwidth to Q, in practice it doesn’t even matter, especially for old video games that have a strong luminance signal. The difference between 1.3 MHz and 0.6 MHz is so small that it’s almost imperceptible. You can only really notice it by looking only at the chroma signal directly.

I’d also like to say that for me, personally, I have changed in regards to how I see color handling. I have found that the environment has as much, if not more, of an effect on how we see color as the screen itself. For example, the overall brightness of the screen compared to the room effects how we perceive gamma. Our eyes have an incredible ability to adjust perception, and things like color temperature and overall brightness don’t matter that much. A CRT and LCD with identical picture may end up looking different just by virtue of what environment they are in. Therefore, it is more important to optimize the viewing environment first if one is interested in comparing colors at a high precision.

4 Likes

Quick update, I’ve been mainly just tweaking the shader parameters around. I added an encoding filter to mimic behavior on the SNES. However, the filters on the SNES’s video outputs seem too wide to have a visible effect. I also fixed the 2-tap and 3-tap comb filters. They are very blurry. However, I remember TVs with comb filters to be quite blurry. I personally prefer either the feedback filter or the mixed notch and 1-tap comb filter.

Here is a shot of the 2-tap comb filter.

3 Likes

I think notch filters are generally preferable for retro gaming. Comb filters just seem to suck in general with high frequency content, but their properties make them preferable for watching shows/movies/etc.

I think it’s

3d adaptive comb filter > notch filter > other comb filter

1 Like

There is the issue with interlaced 480i too, it will grab 2 lines away as the next one is not available (blank). It should be even worse in 480i. At least i am doing it this way.

Most of these things me, you and everyone doing is assuming, guessing or whatever, as there is no clear answer or documentation. Add the GLSL/SLANG/LCD quirks to the equation. In any case it’s a faithful reproduction of the effect as much as can be done with the minimum info available.

3 Likes