CMATRIX · Volume 3

How the Rain Falls

The animation looks like a physical simulation — streams of glyphs falling at different speeds, fading out, new ones starting. It is nothing of the kind. There is no velocity, no gravity, no particle, and no stream object. There is a two-dimensional array of cells, three small arrays of per-column bookkeeping, and a loop that shuffles values downward. Everything the eye reads as motion is produced by which cells hold a character and which hold a space.

This volume reads the algorithm out of cmatrix.c as shipped in version 2.0.

3.1 The data

The whole of the program’s state is four declarations:

typedef struct cmatrix {
    int val;        /* character code, or -1 / ' ' for empty */
    bool is_head;   /* true for the leading glyph of a stream */
} cmatrix;

cmatrix **matrix;   /* [LINES+1][COLS] — the screen buffer */
int *length;        /* [COLS] — how long this column's stream is */
int *spaces;        /* [COLS] — empty cells still to be laid down */
int *updates;       /* [COLS] — this column's speed class, 1..3 */

LINES and COLS are ncurses globals holding the terminal’s current size. var_init() allocates the buffer as a single block with a row-pointer index, then seeds the per-column arrays with random values:

for (j = 0; j <= COLS - 1; j += 2) {
    spaces[j]  = (int) rand() % LINES + 1;
    length[j]  = (int) rand() % (LINES - 3) + 3;
    matrix[1][j].val = ' ';
    updates[j] = (int) rand() % 3 + 1;
}

Two details of that loop set the character of the whole effect. The step is j += 2, so only even-numbered columns ever carry a stream — the rain is drawn on every other column, which is why it reads as vertical ribbons rather than a solid block of text. And length[j] is drawn from 3 to LINES - 1, so stream lengths vary from three characters to nearly a full screen height.

Figure 1 — The per-column model. Each even column carries one stream: a run of length[j] glyphs led by a head cell, with spaces[j] empty cells above it. Odd columns are never written.
Figure 1 — The per-column model. Each even column carries one stream: a run of length[j] glyphs led by a head cell, with spaces[j] empty cells above it. Odd columns are never written. — Drawn for this deep dive from the cmatrix 2.0 source; generator script kept with the project.

3.2 The main loop

One pass of the loop does four things in order: check for signals, poll for a keystroke, advance every column, redraw every cell. Then it sleeps for napms(update * 10) and starts again.

3.2.1 Advancing a column

The default path — the code calls it “new style scrolling” — does not shift the array. It walks the column from the top, finds the end of the existing run of glyphs, and writes one new character below it:

/* skip over spaces */
while (i <= LINES && (matrix[i][j].val == ' ' || matrix[i][j].val == -1))
    i++;
...
/* walk to the end of this run */
z = i;                       /* remember where the run started */
while (i <= LINES && matrix[i][j].val != ' ' && matrix[i][j].val != -1) {
    matrix[i][j].is_head = false;
    i++; y++;                /* y counts the run's current length */
}
matrix[i][j].val = (int) rand() % randnum + randmin;
matrix[i][j].is_head = true;

if (y > length[j] || firstcoldone) {
    matrix[z][j].val = ' ';  /* erase the topmost glyph */
    matrix[0][j].val = -1;
}

The stream therefore grows one cell downward per pass, and once it has reached its assigned length[j] it also loses one cell from the top per pass. A fixed-length run of characters walks down the column — the illusion of a falling object, produced entirely by adding at one end and erasing at the other. The is_head flag is cleared from the whole run and then set on the newly written bottom cell, so the leading glyph is always the freshest one.

-o (“old-style scrolling”) selects a genuinely different mechanism, retained from the early versions: it shifts every cell in the column down by one and writes a new value into row zero, letting a state machine on matrix[1][j] decide whether that value is a glyph, a space, or the special head marker. The README notes that this older mode is the one that looks most like the original Windows and Mac screensaver, invoked as cmatrix -ol. The source comment on the branch is less diplomatic: /* I don't like old-style scrolling, yuck */.

3.2.2 Different speeds

Every column is advanced on the same pass, which would make every stream fall at exactly the same rate. The -a (asynchronous) flag introduces the variation:

count++;
if (count > 4) count = 1;
...
if ((count > updates[j] || asynch == 0) && pause == 0) { /* advance this column */ }

count cycles 1, 2, 3, 4. Each column has a fixed updates[j] of 1, 2 or 3. A column with updates[j] == 1 advances on three passes out of four; one with updates[j] == 3 advances on one pass in four. That is the entire speed model — three discrete rates, produced by a modulo counter and a comparison, and it is enough to destroy the visual lockstep completely.

3.2.3 Which characters

The glyph range is chosen once, at startup, from the mode flags:

Table 1 — The glyph range is chosen once, at startup, from the mode flags

ModeRangeNotes
default33 – 123printable ASCII, punctuation through {
-c classic0xFF66 – 0xFF9Dhalf-width kana, the film’s character set
-l / -x166 – 217indices into the special matrix console font

A new random value in that range is drawn for each new head cell. By default the rest of a stream never changes once written; -k makes each existing glyph mutate with probability 1 in 8 per pass, which is what produces the “code churning in place” look.

3.2.4 Drawing

The redraw is unconditional and total: every cell of every column, every frame.

for (i = y; i <= z; i++) {
    move(i - y, j);
    if (matrix[i][j].val == 0 || (matrix[i][j].is_head && !rainbow)) {
        attron(COLOR_PAIR(COLOR_WHITE));    /* the leading glyph is white */
        ...

Two rules give the effect its signature. The head cell is drawn in white rather than the stream colour — the bright leading character that reads as the “front” of the fall. And when -b (bold) is active without -B, boldness is decided by matrix[i][j].val % 2 == 0: the character code’s parity, not a random draw. Half the glyphs are bright and half are dim, deterministically, which is why the texture stays stable instead of shimmering.

Figure 2 — Rainbow mode (-r) with a centred message. In rainbow mode the head cell loses its white highlight, because the colour is re-randomised per cell before drawing. The message here is lock mode's defau…
Figure 2 — Rainbow mode (-r) with a centred message. In rainbow mode the head cell loses its white highlight, because the colour is re-randomised per cell before drawing. The message here is lock mode's default text. — File:CMatrix 01.png by NairobiPapel. CC BY-SA 4.0, via Wikimedia Commons.

3.3 Interaction, resizing and exit

Keys are polled with wgetch() on a zero timeout, so the loop never blocks. Most of the command-line flags have a live equivalent: a toggles asynchronous scrolling, b/B/n change boldness, 0–9 set the frame delay, the shifted number keys pick a colour, p pauses, q quits.

Terminal resizing is handled through signals rather than polling. SIGWINCH sets a flag; the loop notices it, re-reads the size with the TIOCGWINSZ ioctl, calls resizeterm() and wresize(), reallocates every buffer through var_init(), and clears the screen. The floor of ten rows and ten columns in that path prevents a pathologically small window from producing a zero-sized allocation.

Exit is equally deliberate. finish() restores the cursor, clears the screen, calls resetty() and endwin(), and — if a console font was loaded — runs setfont or consolechars again to put the original font back. A curses program that skips this leaves the shell in a broken state, with no echo and no line editing. Lock mode (-L) works by refusing that path: SIGINT, SIGQUIT and SIGTSTP are all caught and ignored while the flag is set.

3.4 Why it costs so much CPU

The manual page’s warning that the program “is very CPU intensive” and can consume “over 40% of your CPU at times” follows directly from the design. Every frame the program walks and redraws every cell of the terminal — on a 200 × 50 window that is 10,000 cells, twenty-five times a second, each one a move() and an addch() or addwstr() call. ncurses will optimise the resulting escape-sequence traffic, but the per-cell work in the application happens regardless, and the character values change constantly enough that little of it can be elided.

Raising the delay with -u is the real control: -u 9 gives a 90 ms frame interval, less than half the default frame rate. The cost is not a bug to be fixed but the arithmetic of a full-screen character animation written in the most straightforward way possible — which, for a program whose output is decorative, was always the right trade.

Comments (0)

  1. Loading…

Comments are held for moderation — nothing appears until approved.