Skip to content
ISSOtm edited this page Jun 21, 2024 · 6 revisions

This is a description of how gb-vwf works internally. Treat this as a guide to hacking it, or a starting place if you want to contribute.

Note

Last updated as of commit a9229398da90aac7c51b8fdbfab7f005cf0ea594. Like all internals, it is subject to change, and sometimes this page is forgotten about... Please open an issue if this appears to be outdated.

Components

The first thing to consider is that the VWF engine is split in three parts. The "reader", the "pen", and the "printer".

Pen

The "pen" component is the main one, and is tasked with "painting" glyphs onto tiles. It is what gets ticked by TickVWFEngine.

Source stack

The engine maintains a stack of where text should be read from.

The active entry is the topmost one (unlike in v1, there is no separate place where the current read address is stored); the engine is done when there are zero entries on the stack.

The stack only contains pointers. The source bank is, instead, a global variable. (In short, because three-byte entries are a pain to iterate through, and it seems reasonable to require all related text to live in the same ROM bank or in RAM.)

Control characters

Characters with bit 7 set are control characters. They index into a "reversed" table of pointers to their handlers.

Glyph printing

Glyphs are printed to a buffer in WRAM, made of two 2bpp tiles. A WRAM buffer is used in order to save on VRAM wait time (since reads happen as well as writes!), and to avoid showing partially-updated tiles.

Since a glyph can straddle two tiles, the buffer contains two tiles. And since individual characters can have different colours, the buffer itself is 2bpp, instead of 1bpp "expanded" during copy to VRAM.

Colour is applied by toggling whether the glyph is applied to the high bitplane (the low bitplane is always written to).

Breakable characters

Exactly two characters are breakable: , and -. After printing them, the engine checks if a line-break should occur (see Lookahead below); if yes:

  1. If the character is a space, then its printing is undone; the pixel buffer is untouched, as it is assumed that a space is, well, blank.
  2. The newline control character handler is called.

Lookahead

The "newline lookahead" is its own component, but it's exclusively called into by the "pen", and somewhat cooperates with it. It is essentially composed of the ShouldBreakLine function.

It is, in a sense, a specialised reimplementation of the pen (which is only ~half as big as the code composing the "pen"!).

"Shadow" source stack

The lookahead needs to look into the future, essentially. Since the engine incorporates a call-like mechanism, the lookahead needs to simulate the source stack, but without touching the actual source stack.

For performance reasons, a full copy of the stack isn't made! Instead, the lookahead only writes to the entries that are not used by the "main" source stack (i.e. those "above" it), and refuses to write to the "active" entries.

For this reason, it is sometimes impossible for it to continue (fall below the "main" stack's top, then encounter a call, and then attempt to return from that). When this happens, the lookahead conservatively assumes that no line break is necessary, but it can't be sure.

(Likewise, the lookahead has a "shadow" copy of the active font ID and a few other variables, since that may get updated mid-lookahead.)

Control character length

The lookahead needs to be able to process control chars as well. There are two categories of control chars in this regard:

  • Control chars that the lookahead cares about (e.g. anything to do with the font, or flow control). Those are "special" control chars, and their processing is hardcoded into the lookahead.
  • Control chars that the lookahead doesn't care about (e.g. changing output colour). These can simply be skipped. To do that efficiently, the lookahead relies on a table of how many operand bytes each control character uses.

Printer

The "printer" component writes tile IDs to the tilemap to make the pen's output visible on-screen. It is what gets ticked by PrintVWFChars.

Flushing

If the current tile is "full" (= at least 8 pixels have been printed), then it is written to VRAM, and the tile buffer is shifted left by 8 pixels. (The new tile is made blank, of course.)

The active tile ID is then incremented (including wrap-around).

Tilemap updates

If the tile was flushed, then its tile ID is written to VRAM at the "printer's head".

If a newline has just been executed by the "pen", then the "printer head" moves back to the left of the textbox.

If this goes beyond the bottom of the textbox, the entire rectangle of the tilemap covered by the textbox is shifted upwards by 1 row (and the bottom row is filled by VWF_EMPTY_TILE_ID).

Residual tile update

The current tile may be written to VRAM at this point, but without advancing the tile ID.

This is done if all of these criteria are met:

  • The print delay is not 0 (because it's likely that the tile will be filled on the next tick, and this would be a great unnecessary overhead if insta-printing). But, if the "pen" is done (= there are 0 stack entries left), then the print delay is ignored, because there will be no "next tick".
  • The tile is not blank. Again, in the interest of performance, no check is performed for the tile's actual contents; instead, it's assumed that if 0 or 1 pixels have been printed, then the tile must be blank. (1 is accepted because the rightmost column of pixels of all glyphs is blank.)

If a residual tile update needs to happen, then it is written to VRAM, and its tile ID to the tilemap (again, without advancing the tile ID).