pakoito / cheap-upscaling-triangulation

A simple, fast and efficient algorithm to upscale retro games

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cheap Upscaling Triangulation

Cheap Upscaling Triangulation (CUT) is a simple, single-image upscaling algorithm for retro games designed to be:

  • Versatile: it can upscale from and to any image resolution and is applicable to all the 2D and 3D consoles that Lemuroid supports
  • Efficient: battery consumption is very critical on mobile devices, so it leverages the GPU and keeps the number of samples and calculations as low as possible

In order to achieve this, we need to CUT some corners... Literally!

The Intuition

The first Pixel-Art scaling algorithms were cutting corners of input pixels when the two diagonal neighbors had the same color. This smoothed out 45° and 135° straight lines increasing the perceived resolution. Can we extend the idea in continuous space and make it fast?

The first implementation of CUT was actually doing this, but it started to show its limits with newer consoles. The solution needed to be more general.

Algorithm

Triangulation

Triangulation is often used when upscaling images. In CUT, for each output pixel, we sample the 2x2 neighborhood and, compute the luminance of these four input pixels.

When the difference in luminance on one diagonal is much smaller compared to the other, we cut the pixel on that diagonal, creating two triangles.

Input Image Triangulated Pixels Chosen Triangulation

Interpolation

The output of the first step is a series of triangles and squares, where each vertex is associated with an input pixel color. We can mix these colors using standard bilinear interpolation on squares and barycentric coordinates interpolation on triangles.

Changing the interpolation function provides different levels of sharpness. You can see here the difference between two extremes: step and linear interpolations.

Triangulation Step Interpolation Linear Interpolation

Dynamic Sharpness

When looking at 8-bit Pixel-Art, we definitely want these edges to be as sharp as possible, but as we start moving to 16-bit, gradients and bitmaps start to look noisy.

CUT tries to solve this by measuring local contrast using the Michelson formula on the input pixels and adjusts the interpolation function to produce sharper edges where the contrast is high and smoother edges where it's low.

This increases the perceived resolution on edges, limiting noise or bands in gradients. These sharpness values can be tailored to the content displayed.

Input CUT (Static Sharpness) CUT (Dynamic Sharpness)

Implementation

The implementation is provided as a GLSL shader, and it comes with a couple of useful optimizations:

  • Instead of computing barycentric coordinates for the two triangles of each of the two triangulations, we move coordinates and points so that only one is calculated for each output fragment
  • Instead of computing the luminance of each input pixel, we take the green channel, which provides a good enough estimate and saves us four dot products

The shader exposes a few parameters which can be used to customize the behaviour:

#define USE_DYNAMIC_SHARPNESS 1 // Set to 1 to use dynamic sharpening
#define USE_SHARPENING_BIAS 1 // Set to 1 to bias the interpolation towards sharpening
#define DYNAMIC_SHARPNESS_MIN 0.10 // Minimum amount of sharpening in range [0.0, 0.5]
#define DYNAMIC_SHARPNESS_MAX 0.30 // Maximum amount of sharpening in range [0.0, 0.5]
#define STATIC_SHARPNESS 0.2 // If USE_DYNAMIC_SHARPNESS is 0 apply this static sharpness

Results

Here you can find some results. The left part of the image is obtained with standard nearest-neighbor interpolation, while the right side is computed using two profiles of CUT:

  • For 2D games: (DYNAMIC_SHARPNESS_MIN: 0.10, DYNAMIC_SHARPNESS_MAX: 0.30)
  • For 3D games: (DYNAMIC_SHARPNESS_MIN: 0.00, DYNAMIC_SHARPNESS_MAX: 0.25)

Performances

There aren't yet extensive performance tests, but I tried measuring GPU load on my device, a Galaxy S21 FE with Snapdragon 888 playing Final Fantasy VI Advance.

Filter GPU Utilization Resolution Note
Bilinear (Lemuroid) ~0.8% 1080p --
CRT (Lemuroid) ~1.0% 1080p --
CUT (Lemuroid) ~1.5% 1080p --
HQx2 (Retroarch) ~1.5% 320p Fixed Resolution Increase of 2x
HQx4 (Retroarch) ~2.5% 640p Fixed Resolution Increase of 4x
xbrz-freescale-multipass (Retroarch) ~6.0% 1080p Best image quality on 2D content
xbrz-freescale (Retroarch) ~15% 1080p Best image quality on 2D content

About

A simple, fast and efficient algorithm to upscale retro games

License:MIT License


Languages

Language:GLSL 100.0%