--- title: Recreating the Matrix rain with ANSI escape sequences date: 2025-12-21 layout: post project: true thumbnail: thumb_sm.png --- My 2022 implementation of the Matrix rain had too many loose ends. Unicode support was inflexible: the charset had to be a single contiguous block with no way to mix ASCII with something like Katakana; Phosphor decay level was stored in a dedicated array--still don't understand why I did that when I had already used bit-packing for the RGB channels; The algorithm was difficult to decipher. The 2022 version worked, but that’s not the same thing as being correct. I began by placing the decay factor in the LSB of the 4-byte RGB value. Let's call that RGB-PD. PD plays a somewhat analogous role to an alpha channel; I avoided labelling it A so as not to cause confusion: ``` enum { R, /* Red */ G, /* Green */ B, /* Blue */ PD /* Phosphor decay level */ }; typedef union color_tag { uint32_t value; unsigned char color[4]; } color; ``` The decision to use union over more portable bit twiddling was made three years ago, as I recall, for readability. Seeing as all my systems are little-endian, this is unlikely to cause me trouble. Besides, if union is never to be used, why is it in the language anyway? The blend() function, which emulates the dim afterglow of Phosphor by eroding the RGB channels towards the background, remains as elegant as it did three years ago: ``` #define DECAY_MPLIER 2 static inline void blend(matrix *mat, size_t row, size_t col) { unsigned char *color; color = mat->rgb[index(mat, row, col)].color; color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER; color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER; color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER; } ``` While the memory inefficiency of Phosphor decay tracking was a technical oversight I hadn't noticed, the limitation around mixing nonadjacent Unicode blocks was a nagging concern even three years ago. So, a fix was long overdue. In the new version, I introduced a glyphs array that enables a user to add as many Unicode blocks as they want. The insert_code() function picks a block from the array at random, and then picks a character from that block at random: ``` #define UNICODE(min, max) (((uint64_t)max << 32) | min) static uint64_t glyphs[] = { UNICODE(0x0021, 0x007E), /* ASCII */ UNICODE(0xFF65, 0xFF9F), /* Half-width Katakana */ }; static uint8_t glyphlen = (sizeof glyphs) / (sizeof glyphs[0]); static inline void insert_code(matrix *mat, size_t row, size_t col) { uint64_t block; uint32_t unicode_min, unicode_max; block = glyphs[(rand() % glyphlen)]; unicode_min = (uint32_t)block; unicode_max = (uint32_t)(block >> 32); mat->code[index(mat, row, col)] = rand() % (unicode_max - unicode_min) + unicode_min; } ``` The Unicode blocks are stored in 8-byte containers: the low four bytes form the first codepoint and the high four bytes the last. Here, I chose bitwise operations over unions because, first and foremost, the operations themselves are trivial and idiomatic, and the UNICODE() macro simplifies the management of charsets. The insert_code() function is now ready to take its rightful place next to blend(). The result is a digital rain that captures the original Matrix aesthetic with high visual fidelity: ``` $ cc -O3 main.c -o matrix $ ./matrix ``` There was no cause to measure the program's performance characteristics precisely; it's gentle on the CPU. On my ThinkPad T490 running OpenBSD, which has a resolution of 1920x1080, it uses about 2-3% of the CPU, with occasional jumps of up to about 8%; the cores remain silent, the fans don't whir, the rain falls in quiet. Files: [source.tar.gz](source.tar.gz)