Working on the tiny terminal emulator meant trying to squeeze in a lot of text onto a tiny screen. The terminal display is a 128×128 LCD matrix (one inch square physical size) and I started out using the standard Adafruit GFX monospace pixel font: each character is a 6×8 box. That gave me 16 lines of text, each with 21 characters. That can fit much less content than the standard VT100 display area, which is 80 characters by 24 rows.
Vertical text density is not a worry – 16 lines is enough to show a good amount of console output. However, 21 characters of horizontal space is very limited: a quarter of the standard 80 characters (and that itself is very conservative compared to a typical desktop GUI console window).
This is an example of typical console output for
drwxr-xr-x 24 root root 4096 Sep 18 03:52 .
drwxr-xr-x 24 root root 4096 Sep 18 03:52 ..
drwxr-xr-x 2 root root 12288 Aug 23 01:54 bin
drwxr-xr-x 4 root root 1024 Aug 23 01:59 boot
drwxr-xr-x 19 root root 3840 Oct 7 02:37 dev
drwxr-xr-x 93 root root 4096 Oct 7 02:37 etc
And here it is at 21 characters per line:
drwxr-xr-x 24 root
root 4096 Sep 1
8 03:52 .
drwxr-xr-x 24 root
root 4096 Sep 1
8 03:52 ..
drwxr-xr-x 2 root
root 12288 Aug 2
3 01:54 bin
drwxr-xr-x 4 root
root 1024 Aug 2
3 01:59 boot
drwxr-xr-x 19 root
root 3840 Oct
7 02:37 dev
drwxr-xr-x 93 root
root 4096 Oct
7 02:37 etc
I needed more horizontal text density.
Each LCD pixel is a combo of three colour elements — blue, green and red (RGB physically laid out as BGR). Each colour element is a tiny dot of light, and the element triplets are lined up horizontally, so e.g. 128 pixels comprise a row of 128 * 3 = 384 light dots. Subpixel rendering works by pretending that the colour does not matter and treating each dot as a sort of mini-pixel of its own.
Usually that is utilized for anti-aliasing larger shape edges but I wanted to see if this would work for a pixel font. Dividing 384 dots by 6-pixel font width means 64 characters per line – much better than before.
To make the effect happen I had to combine every three pixels of text font into one pixel by OR-ing them together as RGB components:
// for each character pixel row:
// output 6 font pixels as 2 two screen pixels
// (each pixel is encoded into 16 bits in a 5-6-5 pattern)
(row_pixel_0 ? 0x001F : 0) |
(row_pixel_1 ? 0x07E0 : 0) |
(row_pixel_2 ? 0xF800 : 0)
(row_pixel_3 ? 0x001F : 0) |
(row_pixel_4 ? 0x07E0 : 0) |
(row_pixel_5 ? 0xF800 : 0)
Essentially, the 6-pixel-wide character matrix is fit into a 2-pixel render area, i.e. the 6×8 pixel font ends up being 2×8. Notably, there is no vertical compression, since the colour elements are laid out in horizontal rows.
The image above shows a close-up of what that looks like on the actual physical screen. The individual colour dots can be seen working “independently” and spelling out letters at a much higher resolution.
Getting this sort of density for “free” is of course too good to be true. Here is a less zoomed-in picture of the screen with more text on it:
On camera the screen comes off still appears relatively readable (well… barely), but to the naked eye it definitely looks like a jumbled multi-colour mess. The eye still ends up being confused by the colour of the dots – that is called colour fringing and it is a natural shortcoming of any sub-pixel rendering implementation. In this extreme case it pretty much makes it unusable.
The other problem is that there is no ability to add actual colour to text. With subpixel compressed characters we cannot really modulate the hue anymore since each light dot is forced to use either red, green or blue as its colour.
To improve on the colour fringing, I followed the clues offered by this online sub-pixel rendering experiment documented by my friend Ateş Göral (citing this source reference article). In essence, it meant blurring the subpixels a bit to reduce the colour confusion.
That produced a much “crisper” looking text to the naked eye:
But, ultimately, it was still just way too hard to read for any practical use. And the implementation got a lot more complex and slow because of the blur pass.
All said, it was a very interesting experiment to run. Fitting full-fledged console output on a square-inch screen felt like a pretty succesful hack. Too bad it needs a magnifying glass to read, sort of like in the movie Brazil.