summaryrefslogtreecommitdiffstats
path: root/_log/matrix-digital-rain.md
blob: 8c97ed34a035c940c5f128ddd4cd4c38e571de03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
---
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
```

<video style="max-width:100%;" controls="" poster="poster.png">
  <source src="matrix.mp4" type="video/mp4">
</video>

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)