├── README.md ├── crt-240p-scale-shader.glsl ├── crt-240p-scale-shader.glslp └── doc-img ├── clarkson.jpg ├── hero.png ├── mind-blown.gif ├── raspberry-pi.jpg ├── retropie.png └── super.jpg /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CRT 240p Scale Shader 3 | 4 | ![Hero](doc-img/hero.png?raw=true) 5 | 6 | ## What's this? 7 | 8 | This is a [RetroArch](https://www.retroarch.com/) 9 | [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) shader for scaling a wide 10 | range of emulated consoles, handhelds and arcade systems to look high-quality and 11 | authentic on a standard definition, horizontal, 4:3, CRT TV through a single 240p super 12 | resolution. 13 | 14 | I'm using the shader with a customized [RetroPie](https://retropie.org.uk/) installation on a 15 | [Raspberry Pi](https://www.raspberrypi.org/) 3B with a 16 | [PI2SCART](http://arcadeforge.net/Pi2Jamma-Pi2SCART/PI2SCART::264.html) hat, but it should 17 | work fine with different 240p RGB output solutions or software setups. 18 | 19 | While I cannot claim the results to be absolutely perfect, it has certainly satisfied me, 20 | somebody deeply familiar with how most of the systems in RetroPie look as original hardware on 21 | a CRT or through an OSSC. I went from constantly tweaking my RetroArch scaling 22 | options to simply playing games. 23 | 24 | ## Features 25 | 26 | ![Super](doc-img/super.jpg?raw=true) 27 | 28 | - Supports a wide range of emulators with no need for per-system or per-game tweaking. Set 29 | this up once and play thousands of games without ever thinking about aspect ratios, 30 | resolutions, scaling, centering, shimmering, interpolation, etc. again 31 | - Care is taken to have the authentic looking aspect ratio for all systems 32 | - Never introduce blurring through filtering on the horizontal axis and filtering is never 33 | visible vertically unless downscaling is required 34 | - TATE games will be automatically displayed in the correct orientation and aspect ratio. 35 | Super-sampling is employed to reduce vertical shimmering and a gentle sharpening filter 36 | mitigates the loss of vertical resolution. An optional overscan correction can be applied 37 | as TATE arcade games are generally not designed with consumer CRT overscan in mind 38 | - Displays handhelds with the correct aspect ratio and by centering them on-screen. TATE 39 | handhelds like the WonderSwan and Lynx are supported, including mid-game rotation 40 | - Code is very short, well-documented and simple. It's trivial to add special cases 41 | for a specific resolution or system 42 | - Negligible performance impact 43 | 44 | ## Motivation 45 | 46 | ![Clarkson](doc-img/clarkson.jpg?raw=true) 47 | 48 | If you've ever tried to get a number of RetroArch cores emulating dozens of consoles, 49 | handhelds and arcade systems to look correct on your CRT, you don't need to read this. 50 | You know what a glitchy mess the Settings->Video->Scaling menu is and how ill-equipped 51 | RetroArch scaling is to deal with things like non-square pixel super wide resolutions. 52 | But if you need a reason why this shader exists, let's see what you'd have to do 53 | without it. 54 | 55 | We want want to use a standard definition 4:3 CRT and have configured our system to output 56 | at a reasonable 320x240 resolution. We'll now go through a few typical game resolutions 57 | and see what we have to do to convince RA to output in a matter that looks authentic and 58 | high quality. 59 | 60 | - **320x240** - 61 | Nothing to be done, great! 62 | - **320x224** - 63 | The wrong thing would be to stretch 224 lines to 240. This would give both the wrong 64 | aspect ratio and introduce severe blurring. We can achieve the correct vertical centering 65 | of 224 lines inside 240 with integer scaling, but the image would then not use the full 320 66 | horizontal pixels in each line as the aspect ratio math is done incorrectly on 224 lines. 67 | Even for this simple case already we'd have to use the 'custom' mode and configure the 68 | scaling completely manually 69 | - **320x256** - 70 | Arcade games like R-Type often output in >240p resolutions, we need to downscale. 71 | Unfortunately, RA does not support this very well as the input is not MIP-mapped and 72 | there's no build-in supersampling, so shimmering is unavoidable. The best we can do is 73 | turn on bilinear filtering in the video menu. This can only be done for both axis at the 74 | same time and can introduce a loss of sharpness and brightness in small features on an 75 | axis where it isn't even required. 76 | - **160x144** - 77 | Handheld resolutions like these can only be properly displayed by centering them 78 | on-screen. One complication is that some handhelds do not have square pixels. Time to 79 | pull out the calculator to find the correct image width and on-screen X/Y offsets. 80 | - **256x224** - 81 | There is no way to make this horizontal resolution look good with our chosen output 82 | resolution. We can either assume square pixels and a 8:7 aspect, which would look sharp but 83 | wrong with a squeezed image and black borders or we can stretch 256 to 320 and introduce 84 | severe blurring. At this point we'd have no choice but to detect the system in the 85 | `runcommand-onstart.sh` script and chose a different output resolution for each console. 86 | - **352x240** - 87 | Many consoles like the PC Engine or the PS1 have a wide range of output resolutions and 88 | there is no one resolution correct for the entire library. We need a per-game resolution 89 | database to handle all those systems correctly. But even this doesn't work in all cases as 90 | some games switch resolution mid-game or let the user chose the output resolution (Soldier 91 | Blade on PCE, for instance). The only thing we can do now is to use a CRT super resolution. 92 | 93 | _CRT Super Resolution?_ 94 | 95 | The idea behind resolutions like 1920x240 is that it allows quality scaling for a range of 96 | horizontal resolutions used by different systems. Vertical resolution mismatches like 224 97 | or 192 lines in a 240 output are resolved by centering, but horizontal resolution 98 | mismatches have to be resolved by stretching / scaling. Stretching 256 to 320 pixels will 99 | always look bad, but 352/320/256 pixels can be quite easily stretched to 1920 or 2048 100 | pixels with minimal scaling artifacts, invisible on a typical consumer CRT display. Since 101 | CRTs do not have a fixed horizontal resolution or any hard upper limit it is possible to 102 | drive even standard definition TVs with 1920, 2048, 3840 etc. horizontal pixels. 103 | 104 | Unfortunately, this means our output display will have a non-square pixel aspect ratio, 105 | something RA is not specifically equipped to handle. We'll need to use the 'custom' 106 | scaling mode and do all of the aspect ratio and centering computation ourselves. 107 | - **240x320** - 108 | This is typical for TATE / vertical arcade games. If we want these to display properly 109 | we'll first have to downsize them as nicely as possible (not something RA supports 110 | natively, as discussed earlier) and then we'll have to center and stretch them to 111 | maintain their correct aspect ratio. All this is further complicated by the non-square 112 | pixel aspect ratio of our super resolution. To do this with RA's default scaling pipeline 113 | we'd need a database of which games are TATE, their resolution and then do the math and 114 | custom per-game setup in the `runcommand-onstart.sh` script. Or manually create a custom 115 | per-game configuration for each game. 116 | - **224x144 / 144x224** - 117 | Even if we're willing to do all the manual or script development work above all of this 118 | finally breaks down with systems like the WonderSwan which can be used both horizontally 119 | and vertically and has some games that even switch the handheld orientation mid-game. 120 | 121 | ![Mind Blown](doc-img/mind-blown.gif?raw=true) 122 | 123 | _A typical user trying to figure out the correct width of the image of a 3:4 game 124 | running on a 4:3 TV with a 6:1 pixel aspect ratio_ 125 | 126 | At this point it should be abundantly clear how tedious and complicated configuring 127 | all of this manually is. Thankfully, this shader handles all of the above without any 128 | manual tweaking required or you ever having to get out the calculator to figure out the 129 | correct settings. 130 | 131 | ## Setup 132 | 133 | ![Raspberry Pi](doc-img/raspberry-pi.jpg?raw=true) 134 | 135 | These are the setup instructions for RetroPie 4.7.1 running on a series 3 Raspberry Pi. 136 | 137 | First, we have to setup super resolution output for all emulators. Since EmulationStation 138 | can't deal with these we want to use a standard square pixel resolution for it and only 139 | switch to our super resolution for the actual emulators. 140 | 141 | A good place to do so are the onstart / onend scripts. 142 | 143 | 144 | `/opt/retropie/configs/all/runcommand-onstart.sh`: 145 | 146 | ``` 147 | vcgencmd hdmi_timings 1920 1 79 208 241 240 1 6 10 6 0 0 0 60 0 38400000 1 > /dev/null 148 | tvservice -e "DMT 87" > /dev/null 149 | fbset -depth 8 && fbset -depth 16 -xres 1920 -yres 240 > /dev/null 150 | ``` 151 | 152 | `/opt/retropie/configs/all/runcommand-onend.sh`: 153 | 154 | ``` 155 | vcgencmd hdmi_timings 320 1 11 30 38 240 1 8 3 16 0 0 0 60 0 6400000 1 > /dev/null 156 | tvservice -e "DMT 87" > /dev/null 157 | fbset -depth 8 && fbset -depth 16 -xres 320 -yres 240 > /dev/null 158 | ``` 159 | 160 | You'll have to adjust your TV's service menu and / or the [display 161 | timings](https://www.reddit.com/user/ErantyInt/comments/g3c98h/crtpiproject_presents_adjusting_hv_position_with/) 162 | used here to center and size the image correctly. 163 | 164 | Next we have to configure RetroArch scaling. Specifically, we want RA to do as little as 165 | possible, fill out the entire screen and not filter anything. We want all the work to 166 | be done by the shader. Here are settings that can be written to the global RA configuration. 167 | 168 | `/opt/retropie/configs/all/retroarch.cfg`: 169 | 170 | ``` 171 | aspect_ratio_index = "23" 172 | custom_viewport_width = "1920" 173 | custom_viewport_height = "240" 174 | custom_viewport_x = "0" 175 | custom_viewport_y = "0" 176 | video_smooth = "false" 177 | video_scale_integer = "false" 178 | ``` 179 | 180 | You can of course also do that manually in the RGUI Settings->Video->... menus by setting 181 | the aspect ratio to 'Custom', dialing in your chosen super resolution and disabling 182 | integer scaling and bilinear filtering. 183 | 184 | By default RGUI doesn't cope well with our super resolution, but we can add 185 | `rgui_aspect_ratio_lock = "3"` or manually change 186 | Settings->User Interface->Appearance->Lock Menu Aspect Ratio->Stretch to fix this. 187 | 188 | Keep in mind that any per-system settings, core or game overrides might overwrite the 189 | global settings. Please read 190 | [this guide](https://retropie.org.uk/forum/topic/22816/guide-retroarch-system-emulator-core-and-rom-config-files) 191 | if you're confused about how to get your settings used everywhere. 192 | 193 | Now all that is left is to configure RA to use the shader. Clone the repository on your 194 | Pi and use RGUI to load the shader with Quick Menu->Shaders->Load Shader Preset. 195 | Finally, save it as a preset to be used by all systems with Save->Save Global Preset 196 | from the same menu. 197 | 198 | That's it, now nearly all your games should display properly without any further tweaking. 199 | 200 | ## More Setup 201 | 202 | ![RetroPie](doc-img/retropie.png?raw=true) 203 | 204 | If you need help setting up your RetroPie CRT system in general, please see my extensive 205 | [notes](https://github.com/blitzcode/retropie-setup-notes/blob/master/notes.txt). 206 | 207 | They cover not only CRT and scaling specific issues but also more general things like 208 | input lag, overclocking, USB sound cards, turbo fire, BIOS files, backups, etc. 209 | 210 | ## TODO / Limitations 211 | 212 | - Game Gear has the wrong aspect ratio as we cannot distinguish it from the GB(C) by the 213 | resolution alone 214 | - There are going to be some unusual arcade games that have resolutions or screen setups 215 | that are not properly handled by this shader (...but adding special case support is 216 | straightforward) 217 | - Using a single resolution will never correctly accommodate arcade games running at 218 | wildly varying refresh rates and scaling on the horizontal axis will not be pixel-perfect 219 | for most systems (not that big of a deal on a typical consumer CRT TV driven at a super 220 | resolution) 221 | - Vertical downscaling could probably use something better than the simple tent + 222 | sharpening combination 223 | - Very low-resolution systems like the Lynx could be pixel doubled and still fit into 240p 224 | 225 | ## Code 226 | 227 | You are encouraged to have a look at the [shader](crt-240p-scale-shader.glsl) source code. It 228 | has less than 100 lines of actual code and is well-documented and easy to understand. 229 | Customizing it should be very simple. 230 | 231 | ## Special Thanks 232 | 233 | I didn't end up using either of these in my personal setup, but both the 234 | [CRTPi Project](https://www.reddit.com/user/ErantyInt/comments/gqz3qo/crtpiproject_project_image_megathread/) 235 | and 236 | [Snap-Shader](https://github.com/ektgit/snap-shader-240p) 237 | were hugely helpful when figuring out how build my own custom setup. 238 | 239 | ## Legal 240 | 241 | This program is published under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). 242 | 243 | ## Author 244 | 245 | Developed by Tim C. Schroeder, visit my [website](http://www.blitzcode.net) to learn more. 246 | 247 | -------------------------------------------------------------------------------- /crt-240p-scale-shader.glsl: -------------------------------------------------------------------------------- 1 | 2 | varying vec2 tex_coord; 3 | varying vec2 filter_offs; 4 | 5 | uniform mat4 MVPMatrix; 6 | uniform int FrameDirection; 7 | uniform int FrameCount; 8 | uniform vec2 OutputSize; 9 | uniform vec2 TextureSize; 10 | uniform vec2 InputSize; 11 | 12 | // Screen rotation in RA is implemented by rotating the output geometry, detect this here 13 | bool is_vertical() { return MVPMatrix[0].y != 0.0; } 14 | 15 | #if defined(VERTEX) 16 | 17 | attribute vec2 TexCoord; 18 | attribute vec2 VertexCoord; 19 | 20 | void main() 21 | { 22 | // Output position in clip space [-1, 1] 23 | vec4 clip_space_pos = MVPMatrix * vec4(VertexCoord, 0.0, 1.0); 24 | 25 | bool is_vertical = is_vertical(); 26 | 27 | // Scale factor for the vertical axis. Do nothing if input / output resolution 28 | // matches, center if input is lower than output, shrink to output if input is higher 29 | // than output. Keep in mind that our geometry might be rotated 30 | float vert_center = min(1.0, (is_vertical ? InputSize.x : InputSize.y) / OutputSize.y); 31 | clip_space_pos.y *= vert_center; 32 | 33 | // Handheld consoles get centered on the screen and have their correct aspect ratio 34 | bool is_handheld = true; 35 | float handheld_ar = 1.0; 36 | if (InputSize.x == 160.0 && InputSize.y == 144.0) handheld_ar = 1.11; // GB(C) / GG * 37 | else if (InputSize.x == 160.0 && InputSize.y == 152.0) handheld_ar = 1.05; // NGP(C) 38 | else if (InputSize.x == 224.0 && InputSize.y == 144.0) handheld_ar = 1.55; // WonderSwan 39 | else if (InputSize.x == 160.0 && InputSize.y == 102.0) handheld_ar = 1.57; // Atari Lynx 40 | else if (InputSize.x == 102.0 && InputSize.y == 160.0) handheld_ar = 0.64; // Atari Lynx Vertical ** 41 | else if (InputSize.x == 240.0 && InputSize.y == 160.0) handheld_ar = 1.50; // GBA 42 | else 43 | is_handheld = false; 44 | // * We unfortunately can't distinguish between the Game Gear and the Nintendo 45 | // handhelds, causing the former to have the wrong aspect ratio as it uses 46 | // non-square pixels. Feel free to change the aspect to 1.33 to reverse the 47 | // situation in favor of Sega's system 48 | // ** This is a weird special case. Lynx seems to be the only system where vertical 49 | // mode does not rotate the image in post, but the emulator actually outputs a 50 | // different resolution. So it's not treated as a vertical system, is_vertical == 51 | // false and we simply treat it as a horizontal system having a tall aspect ratio 52 | 53 | // Fix for 2 & 3 screen wide Darius games. This is not going to look terribly good but 54 | // at least they're playable (sort of) 55 | if (InputSize.x == 640.0 && InputSize.y == 224.0) 56 | clip_space_pos.y /= 2.0; 57 | else if (InputSize.x == 864.0 && InputSize.y == 224.0) 58 | clip_space_pos.y /= 3.0; 59 | 60 | if (is_handheld) 61 | { 62 | clip_space_pos.x = clip_space_pos.x 63 | // This gets us to a square display 64 | * vert_center * (3.0 / 4.0) 65 | // Now it has the same AR as the physical screen of the device 66 | * handheld_ar 67 | // Aspect ratio correction when in vertical orientation 68 | * (is_vertical ? (1.0 / handheld_ar) * (1.0 / handheld_ar) : 1.0); 69 | } 70 | else if (is_vertical) 71 | { 72 | #if 0 73 | // Overscan adjustment for TATE games. Most of these games don't seem to have any 74 | // consideration for the typical overscan present on consumer CRTs and place 75 | // critical elements like score and bomb counters right at the margin. Here we 76 | // shrink the image a little bit so it's not cut off on CRTs calibrated for 77 | // typical home consoles 78 | float overscan_adj = 1.045; 79 | clip_space_pos.x /= overscan_adj; 80 | clip_space_pos.y /= overscan_adj; 81 | #endif 82 | 83 | // Correct the aspect ratio for 3:4 image in 4:3 frame 84 | clip_space_pos.x *= (3.0 / 4.0) * (3.0 / 4.0); 85 | } 86 | 87 | // Setup texture filtering offsets for downscaling. We do this here so we don't have 88 | // to repeat these calculations per-pixel 89 | { 90 | // If we want to properly filter the input texture we need to know the radius 91 | // which one screen pixel (pixel radius / number of output pixels) represents in 92 | // the normalized texture coordinates of the input texture. It's important to keep 93 | // in mind that the input texture might not be fully used, so we have to adjust 94 | // with the input-res-to-texture-size ratio to prevent an oversized filter kernel 95 | // 96 | // Why do we not simply use the automatic derivative functions dFdx() / dFdy() to 97 | // figure out the proper offsets? Because RPi 3B's GPU doesn't support those 98 | float pixel_r = 0.7; 99 | float support = (pixel_r / OutputSize.y) * 100 | (is_vertical ? InputSize.x / TextureSize.x : InputSize.y / TextureSize.y); 101 | 102 | // Make sure we super-sample along the correct axis of the input texture and use 103 | // the filter support we just computed 104 | filter_offs = is_vertical ? vec2(support, 0.0) : vec2(0.0, support); 105 | } 106 | 107 | // Output 108 | gl_Position = clip_space_pos; 109 | tex_coord = TexCoord; 110 | } 111 | 112 | #elif defined(FRAGMENT) 113 | 114 | uniform sampler2D Texture; 115 | 116 | void main() 117 | { 118 | bool is_vertical = is_vertical(); 119 | 120 | // Disable texture filtering on the horizontal axis by sampling at the texel center. 121 | // The super-resolution output in combination with the softness of the CRT already 122 | // takes care of all filtering, additional bilinear lookups just introduce blurring 123 | // and a loss of brightness in slim features. Filtering on the vertical is fine since 124 | // we either match input texture to screen lines perfectly and it has no effect or 125 | // we're downscaling and want filtering. 126 | // 127 | // In vertical mode, disable filtering on the vertical input texture axis as it runs along 128 | // the horizontal screen axis due to the rotated output geometry 129 | vec2 tex_coord_center = tex_coord; 130 | if (is_vertical) 131 | tex_coord_center.y = (floor(tex_coord_center.y * TextureSize.y) + 0.5) / TextureSize.y; 132 | else 133 | tex_coord_center.x = (floor(tex_coord_center.x * TextureSize.x) + 0.5) / TextureSize.x; 134 | 135 | // Do we have to downscale on the vertical axis (high-res arcade games, TATE games)? 136 | // 137 | // Vertical games have the horizontal input texture axis run along the vertical screen axis, 138 | // swap as with the filtering adjustment code above 139 | if (is_vertical ? OutputSize.y < InputSize.x : OutputSize.y < InputSize.y) 140 | { 141 | // Super-sampling with a tent filter and a bit of sharpening. This can never look 142 | // perfect and our filter is rather simplistic, but the result already looks 143 | // significantly better than default RA scaling and strikes a good balance between 144 | // shimmering and blurriness 145 | float sharpen = 0.7; 146 | gl_FragColor = 147 | ( texture2D(Texture, tex_coord_center - filter_offs * 1.5) * -sharpen + 148 | texture2D(Texture, tex_coord_center - filter_offs ) * 0.5 + 149 | texture2D(Texture, tex_coord_center - filter_offs * 0.5) + 150 | texture2D(Texture, tex_coord_center) * 1.5 + 151 | texture2D(Texture, tex_coord_center + filter_offs * 0.5) + 152 | texture2D(Texture, tex_coord_center + filter_offs ) * 0.5 + 153 | texture2D(Texture, tex_coord_center + filter_offs * 1.5) * -sharpen 154 | ) * (1.0 / (4.5 - 2.0 * sharpen)); 155 | } 156 | else 157 | gl_FragColor = texture2D(Texture, tex_coord_center); 158 | } 159 | 160 | #endif 161 | 162 | -------------------------------------------------------------------------------- /crt-240p-scale-shader.glslp: -------------------------------------------------------------------------------- 1 | shaders = "1" 2 | feedback_pass = "0" 3 | shader0 = "crt-240p-scale-shader.glsl" 4 | filter_linear0 = "true" 5 | wrap_mode0 = "clamp_to_edge" 6 | mipmap_input0 = "false" 7 | -------------------------------------------------------------------------------- /doc-img/clarkson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzcode/crt-240p-scale-shader/fb38b988072e0b686c7d0214f91c5227b9311d81/doc-img/clarkson.jpg -------------------------------------------------------------------------------- /doc-img/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzcode/crt-240p-scale-shader/fb38b988072e0b686c7d0214f91c5227b9311d81/doc-img/hero.png -------------------------------------------------------------------------------- /doc-img/mind-blown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzcode/crt-240p-scale-shader/fb38b988072e0b686c7d0214f91c5227b9311d81/doc-img/mind-blown.gif -------------------------------------------------------------------------------- /doc-img/raspberry-pi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzcode/crt-240p-scale-shader/fb38b988072e0b686c7d0214f91c5227b9311d81/doc-img/raspberry-pi.jpg -------------------------------------------------------------------------------- /doc-img/retropie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzcode/crt-240p-scale-shader/fb38b988072e0b686c7d0214f91c5227b9311d81/doc-img/retropie.png -------------------------------------------------------------------------------- /doc-img/super.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blitzcode/crt-240p-scale-shader/fb38b988072e0b686c7d0214f91c5227b9311d81/doc-img/super.jpg --------------------------------------------------------------------------------