vPLASMA: fold in the real ROM fonts and firmware-confirmed commands

Replaces the guessed font/cursor model with what the firmware dump proved:

- The 8 real Babcock ROM fonts, extracted from tms27pc512.BIN into
  PlasmaFonts.cs (6x8, 6x10, 7x10, 12x16, 12x20). Drops the public-domain
  5x7 stand-in and PlasmaFont.cs.
- Pixel-addressed cursor with the real positioning commands: ESC Q (row Y,
  0-31) and ESC R (column X, 0-127), matching the firmware's range checks.
  Cursor motion (BS/HT/LF/VT/CR) now moves by font pixels, not cells.
- ESC K selects fonts 0-7 (out-of-range ignored, per firmware); ESC H
  attributes are the low 4 bits (half/underline/reverse/flash).

Deferred (documented in FIRMWARE.md): the 10 double-buffered pages
(ESC I/i) and vector-graphics primitives (ESC A-F); vPLASMA keeps a
single-page model for now.

Verified: 24 unit tests pass; the three self-test pages render the real
glyphs (big font 4 banner, full charset in font 0, graphics). Next: begin
the modern-parts hardware replica, with this as the reference firmware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-16 17:39:07 -05:00
co-authored by Claude Opus 4.8
parent 2dca8bfd8f
commit 49e88fbe20
9 changed files with 663 additions and 293 deletions
+7 -6
View File
@@ -30,7 +30,7 @@ internal sealed class PlasmaCanvas : Control
private bool _blinkPhase = true;
private bool _anythingBlinks;
private VPlasmaDevice.Point _cursor;
private int _cursorX, _cursorY;
private PlasmaCursorMode _cursorMode;
private int _cellWidth = 6, _cellHeight = 8;
@@ -62,10 +62,11 @@ internal sealed class PlasmaCanvas : Control
public void UpdateFrom(VPlasmaDevice device)
{
device.CopyFrame(_frame);
_cursor = device.CursorCell;
_cursorX = device.CursorX;
_cursorY = device.CursorY;
_cursorMode = device.CursorMode;
_cellWidth = device.CellWidth;
_cellHeight = device.CellHeight;
_cellWidth = device.FontWidth;
_cellHeight = device.FontHeight;
_anythingBlinks = _cursorMode == PlasmaCursorMode.Flashing;
if (!_anythingBlinks)
@@ -113,8 +114,8 @@ internal sealed class PlasmaCanvas : Control
if (_cursorMode == PlasmaCursorMode.Steady
|| (_cursorMode == PlasmaCursorMode.Flashing && _blinkPhase))
{
int cx = _cursor.Col * _cellWidth;
int cy = _cursor.Row * _cellHeight + _cellHeight - 1;
int cx = _cursorX;
int cy = _cursorY + _cellHeight - 1;
if (cy < VPlasmaDevice.Height)
for (int i = 0; i < _cellWidth && cx + i < VPlasmaDevice.Width; ++i)
g.FillRectangle(lit, Bezel + (cx + i) * _dotPitch, Bezel + cy * _dotPitch, _dotSize, _dotSize);
-130
View File
@@ -1,130 +0,0 @@
namespace VPlasma.Core.Device;
/// <summary>
/// The display's character generator: a classic 5×7 dot-matrix font for
/// ASCII 0x20..0x7E, stored column-major (5 column bytes per glyph, bit 0 =
/// top row) — the layout every KS0108-era controller used. The real panel's
/// ROM glyphs are lost with the hardware; this is the standard public-domain
/// 5×7 set, which is what such panels shipped with. Codes outside the range
/// render as a solid block so stream corruption is visible on the glass.
/// </summary>
public static class PlasmaFont
{
public const int GlyphWidth = 5;
public const int GlyphHeight = 7;
public const byte First = 0x20;
public const byte Last = 0x7E;
/// <summary>
/// Column bits for <paramref name="code"/>'s glyph. Bit r of
/// <c>result[c]</c> is the dot at column c, row r (row 0 at the top).
/// </summary>
public static void GetColumns(byte code, Span<byte> columns)
{
if (code < First || code > Last)
{
columns.Slice(0, GlyphWidth).Fill(0x7F); // solid block
return;
}
Glyphs.AsSpan((code - First) * GlyphWidth, GlyphWidth).CopyTo(columns);
}
private static readonly byte[] Glyphs =
{
0x00, 0x00, 0x00, 0x00, 0x00, // 0x20 ' '
0x00, 0x00, 0x5F, 0x00, 0x00, // 0x21 '!'
0x00, 0x07, 0x00, 0x07, 0x00, // 0x22 '"'
0x14, 0x7F, 0x14, 0x7F, 0x14, // 0x23 '#'
0x24, 0x2A, 0x7F, 0x2A, 0x12, // 0x24 '$'
0x23, 0x13, 0x08, 0x64, 0x62, // 0x25 '%'
0x36, 0x49, 0x55, 0x22, 0x50, // 0x26 '&'
0x00, 0x05, 0x03, 0x00, 0x00, // 0x27 '''
0x00, 0x1C, 0x22, 0x41, 0x00, // 0x28 '('
0x00, 0x41, 0x22, 0x1C, 0x00, // 0x29 ')'
0x08, 0x2A, 0x1C, 0x2A, 0x08, // 0x2A '*'
0x08, 0x08, 0x3E, 0x08, 0x08, // 0x2B '+'
0x00, 0x50, 0x30, 0x00, 0x00, // 0x2C ','
0x08, 0x08, 0x08, 0x08, 0x08, // 0x2D '-'
0x00, 0x60, 0x60, 0x00, 0x00, // 0x2E '.'
0x20, 0x10, 0x08, 0x04, 0x02, // 0x2F '/'
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0x30 '0'
0x00, 0x42, 0x7F, 0x40, 0x00, // 0x31 '1'
0x42, 0x61, 0x51, 0x49, 0x46, // 0x32 '2'
0x21, 0x41, 0x45, 0x4B, 0x31, // 0x33 '3'
0x18, 0x14, 0x12, 0x7F, 0x10, // 0x34 '4'
0x27, 0x45, 0x45, 0x45, 0x39, // 0x35 '5'
0x3C, 0x4A, 0x49, 0x49, 0x30, // 0x36 '6'
0x01, 0x71, 0x09, 0x05, 0x03, // 0x37 '7'
0x36, 0x49, 0x49, 0x49, 0x36, // 0x38 '8'
0x06, 0x49, 0x49, 0x29, 0x1E, // 0x39 '9'
0x00, 0x36, 0x36, 0x00, 0x00, // 0x3A ':'
0x00, 0x56, 0x36, 0x00, 0x00, // 0x3B ';'
0x00, 0x08, 0x14, 0x22, 0x41, // 0x3C '<'
0x14, 0x14, 0x14, 0x14, 0x14, // 0x3D '='
0x41, 0x22, 0x14, 0x08, 0x00, // 0x3E '>'
0x02, 0x01, 0x51, 0x09, 0x06, // 0x3F '?'
0x32, 0x49, 0x79, 0x41, 0x3E, // 0x40 '@'
0x7E, 0x11, 0x11, 0x11, 0x7E, // 0x41 'A'
0x7F, 0x49, 0x49, 0x49, 0x36, // 0x42 'B'
0x3E, 0x41, 0x41, 0x41, 0x22, // 0x43 'C'
0x7F, 0x41, 0x41, 0x22, 0x1C, // 0x44 'D'
0x7F, 0x49, 0x49, 0x49, 0x41, // 0x45 'E'
0x7F, 0x09, 0x09, 0x09, 0x01, // 0x46 'F'
0x3E, 0x41, 0x49, 0x49, 0x7A, // 0x47 'G'
0x7F, 0x08, 0x08, 0x08, 0x7F, // 0x48 'H'
0x00, 0x41, 0x7F, 0x41, 0x00, // 0x49 'I'
0x20, 0x40, 0x41, 0x3F, 0x01, // 0x4A 'J'
0x7F, 0x08, 0x14, 0x22, 0x41, // 0x4B 'K'
0x7F, 0x40, 0x40, 0x40, 0x40, // 0x4C 'L'
0x7F, 0x02, 0x0C, 0x02, 0x7F, // 0x4D 'M'
0x7F, 0x04, 0x08, 0x10, 0x7F, // 0x4E 'N'
0x3E, 0x41, 0x41, 0x41, 0x3E, // 0x4F 'O'
0x7F, 0x09, 0x09, 0x09, 0x06, // 0x50 'P'
0x3E, 0x41, 0x51, 0x21, 0x5E, // 0x51 'Q'
0x7F, 0x09, 0x19, 0x29, 0x46, // 0x52 'R'
0x46, 0x49, 0x49, 0x49, 0x31, // 0x53 'S'
0x01, 0x01, 0x7F, 0x01, 0x01, // 0x54 'T'
0x3F, 0x40, 0x40, 0x40, 0x3F, // 0x55 'U'
0x1F, 0x20, 0x40, 0x20, 0x1F, // 0x56 'V'
0x3F, 0x40, 0x38, 0x40, 0x3F, // 0x57 'W'
0x63, 0x14, 0x08, 0x14, 0x63, // 0x58 'X'
0x07, 0x08, 0x70, 0x08, 0x07, // 0x59 'Y'
0x61, 0x51, 0x49, 0x45, 0x43, // 0x5A 'Z'
0x00, 0x7F, 0x41, 0x41, 0x00, // 0x5B '['
0x02, 0x04, 0x08, 0x10, 0x20, // 0x5C '\'
0x00, 0x41, 0x41, 0x7F, 0x00, // 0x5D ']'
0x04, 0x02, 0x01, 0x02, 0x04, // 0x5E '^'
0x40, 0x40, 0x40, 0x40, 0x40, // 0x5F '_'
0x00, 0x01, 0x02, 0x04, 0x00, // 0x60 '`'
0x20, 0x54, 0x54, 0x54, 0x78, // 0x61 'a'
0x7F, 0x48, 0x44, 0x44, 0x38, // 0x62 'b'
0x38, 0x44, 0x44, 0x44, 0x20, // 0x63 'c'
0x38, 0x44, 0x44, 0x48, 0x7F, // 0x64 'd'
0x38, 0x54, 0x54, 0x54, 0x18, // 0x65 'e'
0x08, 0x7E, 0x09, 0x01, 0x02, // 0x66 'f'
0x0C, 0x52, 0x52, 0x52, 0x3E, // 0x67 'g'
0x7F, 0x08, 0x04, 0x04, 0x78, // 0x68 'h'
0x00, 0x44, 0x7D, 0x40, 0x00, // 0x69 'i'
0x20, 0x40, 0x44, 0x3D, 0x00, // 0x6A 'j'
0x7F, 0x10, 0x28, 0x44, 0x00, // 0x6B 'k'
0x00, 0x41, 0x7F, 0x40, 0x00, // 0x6C 'l'
0x7C, 0x04, 0x18, 0x04, 0x78, // 0x6D 'm'
0x7C, 0x08, 0x04, 0x04, 0x78, // 0x6E 'n'
0x38, 0x44, 0x44, 0x44, 0x38, // 0x6F 'o'
0x7C, 0x14, 0x14, 0x14, 0x08, // 0x70 'p'
0x08, 0x14, 0x14, 0x14, 0x7C, // 0x71 'q'
0x7C, 0x08, 0x04, 0x04, 0x08, // 0x72 'r'
0x48, 0x54, 0x54, 0x54, 0x20, // 0x73 's'
0x04, 0x3F, 0x44, 0x40, 0x20, // 0x74 't'
0x3C, 0x40, 0x40, 0x20, 0x7C, // 0x75 'u'
0x1C, 0x20, 0x40, 0x20, 0x1C, // 0x76 'v'
0x3C, 0x40, 0x30, 0x40, 0x3C, // 0x77 'w'
0x44, 0x28, 0x10, 0x28, 0x44, // 0x78 'x'
0x0C, 0x50, 0x50, 0x50, 0x3C, // 0x79 'y'
0x44, 0x64, 0x54, 0x4C, 0x44, // 0x7A 'z'
0x00, 0x08, 0x36, 0x41, 0x00, // 0x7B '{'
0x00, 0x00, 0x7F, 0x00, 0x00, // 0x7C '|'
0x00, 0x41, 0x36, 0x08, 0x00, // 0x7D '}'
0x08, 0x04, 0x08, 0x10, 0x08, // 0x7E '~'
};
}
+450
View File
@@ -0,0 +1,450 @@
// AUTO-GENERATED from the PD01D221 firmware (tms27pc512.BIN) by
// PlasmaNew tooling. The real Babcock character generator, all 8 fonts.
// Each glyph row is a ushort; bit 15 = leftmost pixel. See PlasmaNew/FIRMWARE.md.
namespace VPlasma.Core.Device;
/// <summary>The display's 8 built-in fonts, extracted from ROM.</summary>
public static class PlasmaFonts
{
/// <summary>One font face: character range, cell size, and packed glyph rows.</summary>
public sealed class Face
{
public int Index { get; }
public byte First { get; }
public byte Last { get; }
public int Width { get; }
public int Height { get; }
private readonly ushort[] _rows; // (c-First)*Height + row; bit15 = leftmost
public Face(int index, byte first, byte last, int width, int height, string hex)
{
Index = index; First = first; Last = last; Width = width; Height = height;
_rows = new ushort[hex.Length / 4];
for (int i = 0; i < _rows.Length; i++)
_rows[i] = (ushort)((HexNibble(hex[i*4]) << 12) | (HexNibble(hex[i*4+1]) << 8) | (HexNibble(hex[i*4+2]) << 4) | HexNibble(hex[i*4+3]));
}
private static int HexNibble(char c) => c <= '9' ? c - '0' : (c & 0x5F) - 'A' + 10;
/// <summary>True if this face has a glyph for <paramref name="code"/>.</summary>
public bool Has(byte code) => code >= First && code <= Last;
/// <summary>Row <paramref name="row"/> of glyph <paramref name="code"/> (bit15 = leftmost), or 0.</summary>
public ushort Row(byte code, int row)
=> Has(code) && (uint)row < (uint)Height ? _rows[(code - First) * Height + row] : (ushort)0;
}
// font 0: 6x8, chars 0x20-0xFF, 224 glyphs
private static readonly Face F0 = new(0, 0x20, 0xFF, 6, 8,
"00000000000000000000000000000000200020002000200000000000200000005000500050000000000000000000000050005000F8005000F8005000"
+ "5000000020007800A00070002800F00020000000C000C80010002000400098001800000060009000A0004000A8009000680000006000200040000000"
+ "0000000000000000100020004000400040002000100000004000200010001000100020004000000000002000A8007000A80020000000000000002000"
+ "2000F800200020000000000000000000000000006000200040000000000000000000F800000000000000000000000000000000000000600060000000"
+ "00000800100020004000800000000000700088009800A800C80088007000000020006000200020002000200070000000700088000800100020004000"
+ "F8000000F80010002000100008000800F00000001000300050009000F800100010000000F8008000F000080008000800F0000000300040008000F000"
+ "8800880070000000F8000800100020004000400040000000700088008800700088008800700000007000880088007800080010006000000000006000"
+ "600000006000600000000000000060006000000060002000400000001000200040008000400020001000000000000000F8000000F800000000000000"
+ "80004000200010002000400080000000700088000800100020000000200000007000880008006800A800A800700000007000880088008800F8008800"
+ "88000000F00088008800F00088008800F000000070008800800080008000880070000000E00090008800880088009000E0000000F80080008000F000"
+ "80008000F8000000F80080008000F0008000800080000000700088008000B8008800880070000000880088008800F800880088008800000070002000"
+ "2000200020002000700000003800100010001000100090006000000088009000A000C000A000900088000000800080008000800080008000F8000000"
+ "8800D800A800A800880088008800000088008800C800A800980088008800000070008800880088008800880070000000F00088008800F00080008000"
+ "800000007000880088008800A800900068000000F00088008800F000A000900088000000780080008000700008000800F0000000F800200020002000"
+ "20002000200000008800880088008800880088007000000088008800880088008800500020000000880088008800A800A800A8005000000088008800"
+ "50002000500088008800000088008800880050002000200020000000F80008001000200040008000F800000070004000400040004000400070000000"
+ "000080004000200010000800000000007000100010001000100010007000000020005000880000000000000000000000000000000000000000000000"
+ "FC000000600040002000000000000000000000000000000070000800780088007800000080008000B000C80088008800F00000000000000070008000"
+ "8000880070000000080008006800980088008800780000000000000070008800F8008000700000001000280020007000200020002000000000000000"
+ "780088007800080070000000800080008000B000C8008800880000000000200000002000200020002000000010000000300010001000900060000000"
+ "800080009000A000C000A000900000006000200020002000200020007000000000000000D000A800A80088008800000000000000B000C80088008800"
+ "88000000000000007000880088008800700000000000B000C8008800F0008000800000000000680098008800780008000800000000000000B000C800"
+ "8000800080000000000000007800800070000800F000000000002000F800200020002800100000000000000088008800880098006800000000000000"
+ "8800880088005000200000000000000088008800A800A800500000000000000088005000200050008800000000000000880050002000200040000000"
+ "00000000F800100020004000F80000003000400040008000400040003000000020002000200000002000200020000000600010001000080010001000"
+ "6000000008007000800000000000000000000000FC00FC00FC00FC00FC00FC00FC00FC00000000000000F80000000000000000002000200020002000"
+ "2000200020002000200020002000E0000000000000000000000000000000700020002000200020002000200020007000000000000000000000000000"
+ "0000E000200020002000200020002000200070002000200020002000200020002000E0002000200020002000200020002000F8002000200020002000"
+ "200020002000F8000000000000000000000000000000F800200020002000200000000000000008001000200020002000000000000000800040002000"
+ "200020002000200040008000000000000000000020002000100008000000000000000000600060006000F800F800600060006000000000000000F800"
+ "F80000000000000060006000600040006000600060007000600060006000E000E0000000000000000000000000007800780060006000700060006000"
+ "600078007800000000000000000000000000E000E00060006000600060006000600078007800600060006000600060006000E000E000600060006000"
+ "000020007000A80020002000200020000000000040002000F0002000400000000000000020004000F8004000200000000000000020002000E0000000"
+ "000000000000200020002000A80070002000000000002000500088005000500070000000000020007000F800700070007000000000004000F800F800"
+ "F800400000000000000090009000080008009000E00000000000000000000000E000A000E00000000000700040004000400000000000000000000000"
+ "0000400040004000C000000000000000000000008000400020000000000000000000C000C0000000000000000000F8000800F8000800100020000000"
+ "00000000F8000800300020004000000000000000100020006000A00020000000000000002000F8008800080030000000000000000000F80020002000"
+ "F8000000000000001000F8003000500090000000000000004000F8004800500040000000000000000000700010001000F800000000000000F0001000"
+ "F0001000F0000000000000000000A800A800080030000000000000000000F0000000000000000000F800080028003000200020004000000008001000"
+ "20006000A0002000200000002000F800880088000800100020000000F80020002000200020002000F80000001000F800100030005000900010000000"
+ "4000F8004800500048004800900000002000F8002000F800200020002000000000000000780050008800100020000000400078009000100010001000"
+ "200000000000F8000800080008000800F80000005000F800500050001000200040000000C0000000C800080008001000600000000000F80008001000"
+ "20005000880000004000F800C8005000400040003800000000008800880048000800100060000000000078005000A8001C001000600000001000E000"
+ "2000F80020002000400000000000A800A800A800080010002000000070000000F80020002000200040000000800080008000C000A000800080000000"
+ "20002000F80020002000400080000000000070000000000000000000F80000000000F8000800500020005000800000002000F800100020007000A800"
+ "20000000100010001000100010002000400000000000000040002000900090009000000080008000F800800080008000780000000000F80008000800"
+ "080010006000000000004000A000100008000800000000002000F80020002000A800A800200000000000F8000800080050002000100000000000E000"
+ "0000E0000000E0001000000000002000400080008800F80008000000000008000800500020005000800000000000F8004000F8004000400038000000"
+ "40004000F80048005000400040000000000070001000100010001000F80000000000F8000800F80008000800F800000070000000F800080010002000"
+ "40000000900090009000900010002000400000002000A000A000A000A800A8003000000080008000800088009000A000C000000000000000F8008800"
+ "88008800F80000000000F8008800080008001000200000000000C0000000080008001000E0000000000020009000400000000000000000000000E000"
+ "A000E00000000000000000002000000020004000800090006000000010002000700088008800F8008800000080004000700088008800F80088000000"
+ "50000000700088008800F800880000007000880080008000880070002000600010002000F0008000C0008000F000000080004000F0008000C0008000"
+ "F00000007000880070002000200020007000000050000000700088008800880070000000500000008800880088008800700000000800100070000800"
+ "780088007000000070008800700008007800880070000000800040007000080078008800700000005000000070000800780088007000000000007000"
+ "8000800088007000200060001000000070008800F8008000700000007000880070008800F0008000700000008000000070008800F000800070000000"
+ "080050000000C00040004000E00000007000A80000006000200020007000000080005000000030001000100038000000500020000000600020002000"
+ "700000006800B8000000B000C80088008800000008001000000070008800880070000000700088000000700088008800700000008000400000007000"
+ "880088007000000000005000000070008800880070000000F0008800B00088008800D000B00000002000400000008800880098006800000070008800"
+ "0000880088009800680000008000400000008800880098006800000000005000000088008800980068000000");
// font 1: 6x8, chars 0x40-0x7F, 64 glyphs
private static readonly Face F1 = new(1, 0x40, 0x7F, 6, 8,
"100020004000200010000000F8000000400020001000200040000000F800000020002000F800200020000000F8000000000020000000F80000002000"
+ "00000000200050005000500088008800F800000008001000F8002000F80040008000000020007000A800A800A800A80070002000F800400040004000"
+ "40004000E00000000000F8000000F8000000F80000000000080070008000080070008000000000000800700080000000F80000000000000004000400"
+ "4800A8002800100010000000200050005000500088008800880000007000880008006800980088009000600000000000880050002000500088000000"
+ "000020007000F800700020000000000020007000A800A000A8007000200000000000F800880088008800880088000000700088008800A80088008800"
+ "70000000000000005000A8005000000000000000F80040002000100020004000F8000000300048004800300000000000000000000000000000000000"
+ "000000000000000000000000000000000000000000000000000070008800880088005000D8000000F80000000000700000000000F8000000A800A800"
+ "A80070002000200020000000000010007000A800A800A8007000400000000000000000000000000000000000F4005C00540054005400000000000000"
+ "20007000A8002000200020002000200020002000200020002000A80070002000300048004000F00040004800B0000000000000000000680090009000"
+ "6800000070008800880070008800C800B000000000008000880050002000500088000800700088004000200050008800880070000000000070008800"
+ "6000880070000000000020007000A800A800A800700020000000000088008800500050007000200000000000B000C800880088008800080000000000"
+ "00006000200020003000000000001000A800A800A80070002000000000000000480050006000500048000000C0004000200010002800440044000000"
+ "0000000088008800C800B000800080000000000000008800880050002000000000000000700088008800880070000000880050002000F8002000F800"
+ "2000000000000000F80050005000500048000000700088008800F80088008800700000000000000070008800C800B000800080000000000078009000"
+ "8800880070000000000008007000A00020002000100000000000000000009000480048003000000000000000F80050008800A8005000000000000000"
+ "000050008800A80050000000400030004000300040004000380008002000A800A8007000200020002000000040003000400040004000400038000800"
+ "000010000800FC000800100000000000000020004000FC0040002000000000000000000000000000000000000000000020007000A80020002000A800"
+ "70002000");
// font 2: 6x10, chars 0x20-0xFF, 224 glyphs
private static readonly Face F2 = new(2, 0x20, 0xFF, 6, 10,
"000000000000000000000000000000000000000000002000200020002000000000002000000000000000500050005000000000000000000000000000"
+ "000050005000F8005000F8005000500000000000000020007800A00070002800F0002000000000000000C000C8001000200040009800180000000000"
+ "000060009000A0004000A800900068000000000000006000200040000000000000000000000000000000100020004000400040002000100000000000"
+ "0000400020001000100010002000400000000000000000002000A8007000A80020000000000000000000000020002000F80020002000000000000000"
+ "00000000000000000000000060002000400000000000000000000000F800000000000000000000000000000000000000000000006000600000000000"
+ "00000000080010002000400080000000000000000000700088009800A800C80088007000000000000000200060002000200020002000700000000000"
+ "0000700088000800100020004000F800000000000000F80010002000100008000800F0000000000000001000300050009000F8001000100000000000"
+ "0000F8008000F000080008000800F000000000000000300040008000F000880088007000000000000000F80008001000200040004000400000000000"
+ "000070008800880070008800880070000000000000007000880088007800080010006000000000000000000060006000000060006000000000000000"
+ "00000000600060000000600020004000000000000000100020004000800040002000100000000000000000000000F8000000F8000000000000000000"
+ "0000800040002000100020004000800000000000000070008800080010002000000020000000000000007000880008006800A800A800700000000000"
+ "00007000880088008800F80088008800000000000000F00088008800F00088008800F000000000000000700088008000800080008800700000000000"
+ "0000E00090008800880088009000E000000000000000F80080008000F00080008000F800000000000000F80080008000F00080008000800000000000"
+ "0000700088008000B800880088007000000000000000880088008800F800880088008800000000000000700020002000200020002000700000000000"
+ "0000380010001000100010009000600000000000000088009000A000C000A00090008800000000000000800080008000800080008000F80000000000"
+ "00008800D800A800A80088008800880000000000000088008800C800A800980088008800000000000000700088008800880088008800700000000000"
+ "0000F00088008800F0008000800080000000000000007000880088008800A80090006800000000000000F00088008800F000A0009000880000000000"
+ "0000780080008000700008000800F000000000000000F800200020002000200020002000000000000000880088008800880088008800700000000000"
+ "00008800880088008800880050002000000000000000880088008800A800A800A8005000000000000000880088005000200050008800880000000000"
+ "00008800880088005000200020002000000000000000F80008001000200040008000F800000000000000700040004000400040004000700000000000"
+ "000080004000200010000800000000000000000000007000100010001000100010007000000000000000200050008800000000000000000000000000"
+ "0000000000000000000000000000FC000000000000006000400020000000000000000000000000000000000000007000080078008800780000000000"
+ "000080008000B000C80088008800F0000000000000000000000070008000800088007000000000000000080008003400980088008800780000000000"
+ "00000000000070008800F800800070000000000000001000280020007000200020002000000000000000000000007800880088007800080008007000"
+ "0000800080008000B000C800880088000000000000000000200000002000200020002000000000000000100000003000100010001000100090006000"
+ "0000800080009000A000C000A0009000000000000000600020002000200020002000700000000000000000000000D000A800A8008800880000000000"
+ "000000000000B000C800880088008800000000000000000000007000880088008800700000000000000000000000B000C8008800F000800080008000"
+ "0000000000006800980088007800080008000800000000000000B000C800800080008000000000000000000000007800800070000800F00000000000"
+ "000000002000F80020002000280010000000000000000000000088008800880098006800000000000000000000008800880088005000200000000000"
+ "00000000000088008800A800A80050000000000000000000000088005000200050008800000000000000000000008800880088007800080008007000"
+ "000000000000F800100020004000F8000000000000003000400040008000400040003000000000000000200020002000000020002000200000000000"
+ "00006000100010000800100010006000000000000000080070008000000000000000000000000000FC00FC00FC00FC00FC00FC00FC00FC00FC00FC00"
+ "0000000000000000FC000000000000000000000010001000100010001000100010001000100010001000100010001000F00000000000000000000000"
+ "00000000000000001C001000100010001000100010001000100010001C00000000000000000000000000000000000000F00010001000100010001000"
+ "10001000100010001C00100010001000100010001000100010001000F000100010001000100010001000100010001000FC0010001000100010001000"
+ "1000100010001000FC00000000000000000000000000000000000000FC00100010001000100010000000000000000000040008001000100010001000"
+ "0000000000000000C000200010001000100010001000100010002000C000000000000000000000001000100010000800040000000000000000000000"
+ "380038003800FC00FC00FC003800380038003800000000000000FC00FC00FC0000000000000000003800380038003800380038003800380038003800"
+ "380038003800F800F800F80000000000000000000000000000003C003C003C0038003800380038003800380038003C003C003C000000000000000000"
+ "000000000000F800F800F80038003800380038003800380038003C003C003C003800380038003800380038003800F800F800F8003800380038003800"
+ "000000001000380054001000100010001000000000000000000020001000F8001000200000000000000000000000100020007C002000100000000000"
+ "00000000000010001000F000000000000000000000000000100010001000540038001000000000000000000010002800440028002800380000000000"
+ "00000000100038007C00380038003800000000000000000020007C00FC007C0020000000000000000000000048004800840084004800300000000000"
+ "000000000000000000007000500070000000000000000000380020002000200000000000000000000000000000000000200020002000E00000000000"
+ "00000000000000000000400020001000000000000000000000000000600060000000000000000000000000007C0004007C0004000800100000000000"
+ "0000000000007C00040018001000200000000000000000000000080010003000500010000000000000000000000010007C0044000400180000000000"
+ "00000000000000007C00100010007C000000000000000000000008007C001800280048000000000000000000000020007C0024002800200000000000"
+ "00000000000000003800080008007C000000000000000000000078000800780008007800000000000000000000000000540054000400180000000000"
+ "000000000000000078000000000000000000000000007C00040014001800100010002000000000000000040008001000300050001000100000000000"
+ "000010007C00440044000400080010000000000000007C00100010001000100010007C0000000000000008007C000800180028004800080000000000"
+ "000020007C002400240024002400480000000000000010007C0010007C00100010001000000000000000000000003C00240044000800100000000000"
+ "000020003C004800080008000800100000000000000000007C0004000400040004007C0000000000000028007C002800280008001000200000000000"
+ "0000600000006400040004000800300000000000000000007C000400080010002800440000000000000020007C0024002800200020001C0000000000"
+ "0000000044004400240004000800300000000000000000003C00240054000C00080030000000000000000800700010007C0010001000200000000000"
+ "00000000540054005400040008001000000000000000380000007C001000100010002000000000000000400040004000600050004000400000000000"
+ "0000100010007C0010001000200040000000000000000000380000000000000000007C0000000000000000007C000400280010002800400000000000"
+ "000010007C00080010003800540010000000000000000800080008000800080010002000000000000000000000002000100048004800480000000000"
+ "0000400040007C004000400040003C0000000000000000007C0004000400040008003000000000000000000020005000080004000400000000000000"
+ "000010007C001000100054005400100000000000000000007C0004000400280010000800000000000000000070000000700000007000080000000000"
+ "0000000010002000400044007C000400000000000000000004000400280010002800400000000000000000007C0020007C00200020001C0000000000"
+ "0000200020007C0024002800200020000000000000000000380008000800080008007C0000000000000000007C0004007C00040004007C0000000000"
+ "0000380000007C0004000800100020000000000000004800480048004800080010002000000000000000100050005000500054005400180000000000"
+ "00004000400040004400480050006000000000000000000000007C004400440044007C0000000000000000007C004400040004000800100000000000"
+ "000000006000000004000400080070000000000000000000100048002000000000000000000000000000000070005000700000000000000000000000"
+ "000010000000100020004000480030000000000008001000700088008800F800880088000000000080004000700088008800F8008800880000000000"
+ "50000000700088008800F8008800880000000000000070008800800080008000880070002000600008001000F8008000F00080008000F80000000000"
+ "80004000F8008000F00080008000F8000000000070008800700020002000200020007000000000005000000070008800880088008800700000000000"
+ "500000008800880088008800880070000000000008001000000070000800780088007800000000007000880000007000080078008800780000000000"
+ "800040000000700008007800880078000000000000005000000070000800780088007800000000000000000000007000800080008800700020006000"
+ "08001000000070008800F800800070000000000070008800000070008800F000800070000000000080004000000070008800F8008000700000000000"
+ "0800100040000000C00040004000E0000000000070008800200000006000200020007000000000008000400010000000300010001000380000000000"
+ "28000000100000003000100010003800000000006800B80000007000C800880088008800000000000800100000007000880088008800700000000000"
+ "700088000000700088008800880070000000000080004000000070008800880088007000000000000000500000007000880088008800700000000000"
+ "0000780044005800440044006400D8000000000010002000000088008800880098006800000000007000880000008800880088009800680000000000"
+ "80004000000088008800880098006800000000000000500000008800880088009800680000000000");
// font 3: 6x10, chars 0x40-0x7F, 64 glyphs
private static readonly Face F3 = new(3, 0x40, 0x7F, 6, 10,
"0000100020004000200010000000F800000000000000400020001000200040000000F80000000000000020002000F800200020000000F80000000000"
+ "0000000020000000F800000020000000000000000000200050005000500088008800F80000000000000008001000F8002000F8004000800000000000"
+ "20007000A800A800A800A800A8007000200000000000F80040004000400040004000E0000000000000000000F8000000F8000000F800000000000000"
+ "000008007000800008007000800000000000000000000800700080000000F800000000000000000000000400040004004800A8002800100010000000"
+ "0000200050005000500088008800880000000000000070008800080068008C0088009000600000000000000000008800500020005000880000000000"
+ "0000000020007000F80070002000000000000000000020007000A800A000A80070002000000000000000F80088008800880088008800880000000000"
+ "0000700088008800A800880088007000000000000000000000005000A800500000000000000000000000F80040002000100020004000F80000000000"
+ "000030004800480030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000070008800880088005000D800000000000000F80000000000700000000000F800000000000000A800A800A800700020002000200000000000"
+ "0000100010007000A800A800A80070004000400000000000000000000000000000000000000000000000F4005C005400540000000000000000000000"
+ "20007000A80020002000200020002000200020002000200020002000200020002000A800700020000000300048004000F00040004800B00000000000"
+ "00000000000000006800900090006800000000000000700088008800F0008800C800B000800000000000000080008800500020005000880008000000"
+ "000070008800400020005000880088007000000000000000000070008800600088007000000000000000000020007000A800A800A800700020000000"
+ "0000000000008800880050005000700020002000000000000000B000C800880088008800080000000000000000000000600020002000300000000000"
+ "0000000000001000A800A800A80070002000000000000000000048005000600050004800000000000000C00040002000200050008800880000000000"
+ "00000000000088008800C800B00080008000000000000000000000008800880050002000000000000000000000007000880088008800700000000000"
+ "0000880050002000F8002000F800200000000000000000000000F8005000500050004800000000000000700088008800F80088008800700000000000"
+ "00000000000070008800C800B00080008000000000000000000078009000880088007000000000000000000008007000A00020002000100000000000"
+ "0000000000008000500048004800300000000000000000000000F80050008800A800500000000000000000000000000050008800A800500000000000"
+ "0000400030004000300040004000380008000000000000002000A800A800A80070002000200000000000400030004000400040004000380008000000"
+ "0000000010000800FC00080010000000000000000000000020004000FC00400020000000000000000000000000000000000000000000000000000000"
+ "1000380054001000100010001000540038001000");
// font 4: 12x16, chars 0x20-0x7F, 96 glyphs
private static readonly Face F4 = new(4, 0x20, 0x7F, 12, 16,
"00000000000000000000000000000000000000000000000000000000000000000C000C000C000C000C000C000C000C000C000C00000000000C000C00"
+ "0000000033003300330033003300330000000000000000000000000000000000000000003300330033003300FFC0FFC033003300FFC0FFC033003300"
+ "33003300000000000C000C003FC07FC0CC00CC007F003F800CC00CC0FF80FF000C000C0000000000F000F000F0C0F1C0038007000E001C0038007000"
+ "E3C0C3C003C003C0000000003C007E00C300C700CE00DC0078007800DCC0CFC0C700C7007EC03CC0000000001E001E00060006001C00180000000000"
+ "0000000000000000000000000000000000C001C0038007000E000C000C000C000C000E000700038001C000C000000000300038001C000E0007000300"
+ "03000300030007000E001C003800300000000000000000000C000C00CCC0EDC07F807F80EDC0CCC00C000C000000000000000000000000000C000C00"
+ "0C000C00FFC0FFC00C000C000C000C000000000000000000000000000000000000000000000000000F000F00030003000E000C000000000000000000"
+ "0000000000000000FFC0FFC00000000000000000000000000000000000000000000000000000000000000000000000000F000F000F000F0000000000"
+ "0000000000C001C0038007000E001C0038007000E000C00000000000000000003F007F80E1C0C0C0C1C0C2C0C4C0C8C0D0C0E0C0C0C0E1C07F803F00"
+ "000000000C001C003C000C000C000C000C000C000C000C000C000C003F003F00000000003F007F80E1C0C0C000C001C0038007000E001C0038007000"
+ "FFC0FFC000000000FFC0FFC0038007000E000E000700038001C000C000C001C0FF80FF000000000007000F001F003B0033006300E300C300FFC0FFC0"
+ "030003000300030000000000FFC0FFC0C000C000C000C000FF00FF8001C000C000C001C0FF80FF00000000000F001F0038007000E000C000FF00FF80"
+ "C1C0C0C0C0C0E1C07F803F0000000000FFC0FFC000C001C0038007000E001C00380030003000300030003000000000003F007F80E1C0C0C0C0C0E1C0"
+ "7F807F80E1C0C0C0C0C0E1C07F803F00000000003F007F80E1C0C0C0C0C0E0C07FC03FC000C001C0038007003E003C0000000000000000000F000F00"
+ "0F000F00000000000F000F000F000F000000000000000000000000000F000F000F000F00000000000F000F00030003000E000C000000000000C001C0"
+ "038007000E001C00380038001C000E000700038001C000C0000000000000000000000000FFC0FFC000000000FFC0FFC0000000000000000000000000"
+ "300038001C000E000700038001C001C0038007000E001C0038003000000000003F007F80E1C0C0C000C001C0038007000E000C00000000000C000C00"
+ "000000003F007F80E1C0C0C000C000C038C07CC0CCC0CCC0CCC0CCC07F803F00000000003F007F80E1C0C0C0C0C0C0C0C0C0C0C0FFC0FFC0C0C0C0C0"
+ "C0C0C0C000000000FF00FF80C1C0C0C0C0C0C1C0FF80FF80C1C0C0C0C0C0C1C0FF80FF00000000003F007F80E1C0C0C0C0C0C000C000C000C000C0C0"
+ "C0C0C1C07F803F0000000000FF00FF80C1C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C1C0FF80FF0000000000FFC0FFC0C000C000C000C000FF00FF00"
+ "C000C000C000C000FFC0FFC000000000FFC0FFC0C000C000C000C000FF00FF00C000C000C000C000C000C000000000003F007F80E1C0C0C0C000C000"
+ "C000C7C0C7C0C0C0C0C0E1C07F803F0000000000C0C0C0C0C0C0C0C0C0C0C0C0FFC0FFC0C0C0C0C0C0C0C0C0C0C0C0C0000000003F003F000C000C00"
+ "0C000C000C000C000C000C000C000C003F003F00000000000FC00FC003000300030003000300030003000300C300E7007E003C0000000000C0C0C1C0"
+ "C380C700CE00DC00F800F800DC00CE00C700C380C1C0C0C000000000C000C000C000C000C000C000C000C000C000C000C000C000FFC0FFC000000000"
+ "C0C0E1C0F3C0F3C0DEC0CCC0CCC0C0C0C0C0C0C0C0C0C0C0C0C0C0C000000000C0C0C0C0C0C0E0C0F0C0F8C0DCC0CEC0C7C0C3C0C1C0C0C0C0C0C0C0"
+ "000000003F007F80E1C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0E1C07F803F0000000000FF00FF80C1C0C0C0C0C0C1C0FF80FF00C000C000C000C000"
+ "C000C000000000003F007F80E1C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C2C0E1C07F803F4000000000FF00FF80C1C0C0C0C0C0C1C0FF80FF00DC00CE00"
+ "C700C380C1C0C0C0000000003FC07FC0E000C000C000E0007F003F8001C000C000C001C0FF80FF0000000000FFC0FFC00C000C000C000C000C000C00"
+ "0C000C000C000C000C000C0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0E1C07F803F0000000000C0C0C0C0C0C0C0C0C0C0C0C0"
+ "C0C0C0C0C0C0E1C0738033001E000C0000000000C0C0C0C0C0C0C0C0CCC0CCC0CCC0CCC0CCC0CCC0CCC0CCC07F80330000000000C0C0C0C0C0C0E1C0"
+ "73803F001E001E003F007380E1C0C0C0C0C0C0C000000000C0C0C0C0C0C0C0C0C0C0E1C073803F001E000C000C000C000C000C0000000000FFC0FFC0"
+ "00C001C0038007000E001C0038007000E000C000FFC0FFC0000000003F003F0030003000300030003000300030003000300030003F003F0000000000"
+ "00000000C000E000700038001C000E000700038001C000C000000000000000003F003F0003000300030003000300030003000300030003003F003F00"
+ "000000000C001E003F007380E1C0C0C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ "FFC0FFC0000000000C000E000700038001C000C0000000000000000000000000000000000000000000000000000000003F003F8000C000C03FC07FC0"
+ "C0C0C0C07FC03FC000000000C000C000C000C000C000C000DF00FF80F1C0C0C0C0C0C1C0FF80FF000000000000000000000000003F007F80E1C0C0C0"
+ "C000C000C0C0E1C07F803F000000000000C000C000C000C000C000C03EC07FC0E3C0C0C0C0C0E0C07FC03FC00000000000000000000000003F007F80"
+ "C0C0C0C0FF80FF00C000C0007F003F00000000000F001F8039C030C030003000FC00FC00300030003000300030003000000000000000000000000000"
+ "3FC07FC0C0C0C0C07FC03FC000C000C0FF80FF0000000000C000C000C000C000C000C000DF00FF80E1C0C0C0C0C0C0C0C0C0C0C0000000000C000C00"
+ "000000003C003C000C000C000C000C000C000C003F003F000000000000C000C00000000000C000C000C000C000C000C030C039C01F800F0000000000"
+ "300030003000300030C031C0338037003E003E003700338031C030C0000000003C003C000C000C000C000C000C000C000C000C000C000C003F003F00"
+ "000000000000000000000000D300FF80FFC0CCC0CCC0CCC0CCC0CCC0CCC0CCC0000000000000000000000000CF00FF80F1C0C0C0C0C0C0C0C0C0C0C0"
+ "C0C0C0C00000000000000000000000003F007F80E1C0C0C0C0C0C0C0C0C0E1C07F803F000000000000000000DF00FF80F1C0C0C0C0C0C1C0FF80FF00"
+ "C000C000C000C00000000000000000003EC07FC0E3C0C0C0C0C0E0C07FC03FC000C000C000C000C0000000000000000000000000DF00FF80F1C0C0C0"
+ "C000C000C000C000C000C0000000000000000000000000003FC07FC0C000C0007F003F8000C000C0FF80FF000000000030003000FC00FC0030003000"
+ "300030003000300030C039C01F800F00000000000000000000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0E3C07FC03CC0000000000000000000000000"
+ "C0C0C0C0C0C0C0C0C0C0E1C0738033001E000C00000000000000000000000000C0C0C0C0C0C0C0C0C0C0CCC0CCC0CCC07F8033000000000000000000"
+ "00000000C0C0E1C073803F001E001E003F007380E1C0C0C0000000000000000000000000C0C0E1C073803F001E000C000C001C003800300000000000"
+ "0000000000000000FFC0FFC0030007000E001C0038007000FFC0FFC00000000000C001C003800300030006000C000C00060003000300038001C000C0"
+ "000000000C000C000C000C000C000C00000000000C000C000C000C000C000C0000000000300038001C000C000C0006000300030006000C000C001C00"
+ "380030000000000000C001C00F801F00380030000000000000000000000000000000000000000000FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0"
+ "FFF0FFF0FFF0FFF0FFF0FFF0");
// font 5: 12x20, chars 0x20-0x7F, 96 glyphs
private static readonly Face F5 = new(5, 0x20, 0x7F, 12, 20,
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C000C000C000C000C000C000C000C00"
+ "0C000C00000000000C000C00000000000000000000000000330033003300330033003300000000000000000000000000000000000000000000000000"
+ "000000003300330033003300FFC0FFC033003300FFC0FFC033003300330033000000000000000000000000000C000C003FC07FC0CC00CC007F003F80"
+ "0CC00CC0FF80FF000C000C00000000000000000000000000F000F000F0C0F1C0038007000E001C0038007000E3C0C3C003C003C00000000000000000"
+ "000000003C007E00C300C700CE00DC0078007800DCC0CFC0C700C7007EC03CC00000000000000000000000001E001E00060006001C00180000000000"
+ "00000000000000000000000000000000000000000000000000C001C0038007000E000C000C000C000C000E000700038001C000C00000000000000000"
+ "00000000300038001C000E000700030003000300030007000E001C0038003000000000000000000000000000000000000C000C00CCC0EDC07F807F80"
+ "EDC0CCC00C000C0000000000000000000000000000000000000000000C000C000C000C00FFC0FFC00C000C000C000C00000000000000000000000000"
+ "000000000000000000000000000000000000000000000000000000000F000F00030003000E000C0000000000000000000000000000000000FFC0FFC0"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F000F000F000F000000000000000000"
+ "000000000000000000C001C0038007000E001C0038007000E000C000000000000000000000000000000000003F007F80E1C0C0C0C1C0C2C0C4C0C8C0"
+ "D0C0E0C0C0C0E1C07F803F000000000000000000000000000C001C003C000C000C000C000C000C000C000C000C000C003F003F000000000000000000"
+ "000000003F007F80E1C0C0C000C001C0038007000E001C0038007000FFC0FFC0000000000000000000000000FFC0FFC0038007000E000E0007000380"
+ "01C000C000C001C0FF80FF0000000000000000000000000007000F001F003B0033006300E300C300FFC0FFC003000300030003000000000000000000"
+ "00000000FFC0FFC0C000C000C000C000FF00FF8001C000C000C001C0FF80FF000000000000000000000000000F001F0038007000E000C000FF00FF80"
+ "C1C0C0C0C0C0E1C07F803F00000000000000000000000000FFC0FFC000C001C0038007000E001C003800300030003000300030000000000000000000"
+ "000000003F007F80E1C0C0C0C0C0E1C07F807F80E1C0C0C0C0C0E1C07F803F000000000000000000000000003F007F80E1C0C0C0C0C0E0C07FC03FC0"
+ "00C001C0038007003E003C00000000000000000000000000000000000F000F000F000F00000000000F000F000F000F00000000000000000000000000"
+ "00000000000000000F000F000F000F00000000000F000F00030003000E000C0000000000000000000000000000C001C0038007000E001C0038003800"
+ "1C000E000700038001C000C00000000000000000000000000000000000000000FFC0FFC000000000FFC0FFC000000000000000000000000000000000"
+ "00000000300038001C000E000700038001C001C0038007000E001C00380030000000000000000000000000003F007F80E1C0C0C000C001C003800700"
+ "0E000C00000000000C000C000000000000000000000000003F007F80E1C0C0C000C000C038C07CC0CCC0CCC0CCC0CCC07F803F000000000000000000"
+ "000000003F007F80E1C0C0C0C0C0C0C0C0C0C0C0FFC0FFC0C0C0C0C0C0C0C0C0000000000000000000000000FF00FF80C1C0C0C0C0C0C1C0FF80FF80"
+ "C1C0C0C0C0C0C1C0FF80FF000000000000000000000000003F007F80E1C0C0C0C0C0C000C000C000C000C0C0C0C0C1C07F803F000000000000000000"
+ "00000000FF00FF80C1C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C1C0FF80FF00000000000000000000000000FFC0FFC0C000C000C000C000FF00FF00"
+ "C000C000C000C000FFC0FFC0000000000000000000000000FFC0FFC0C000C000C000C000FF00FF00C000C000C000C000C000C0000000000000000000"
+ "000000003F007F80E1C0C0C0C000C000C000C7C0C7C0C0C0C0C0E1C07F803F00000000000000000000000000C0C0C0C0C0C0C0C0C0C0C0C0FFC0FFC0"
+ "C0C0C0C0C0C0C0C0C0C0C0C00000000000000000000000003F003F000C000C000C000C000C000C000C000C000C000C003F003F000000000000000000"
+ "000000000FC00FC003000300030003000300030003000300C300E7007E003C00000000000000000000000000C0C0C1C0C380C700CE00DC00F800F800"
+ "DC00CE00C700C380C1C0C0C0000000000000000000000000C000C000C000C000C000C000C000C000C000C000C000C000FFC0FFC00000000000000000"
+ "00000000C0C0E1C0F3C0F3C0DEC0CCC0CCC0C0C0C0C0C0C0C0C0C0C0C0C0C0C0000000000000000000000000C0C0C0C0C0C0E0C0F0C0F8C0DCC0CEC0"
+ "C7C0C3C0C1C0C0C0C0C0C0C00000000000000000000000003F007F80E1C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0E1C07F803F000000000000000000"
+ "00000000FF00FF80C1C0C0C0C0C0C1C0FF80FF00C000C000C000C000C000C0000000000000000000000000003F007F80E1C0C0C0C0C0C0C0C0C0C0C0"
+ "C0C0C0C0C2C0E1C07F803F40000000000000000000000000FF00FF80C1C0C0C0C0C0C1C0FF80FF00DC00CE00C700C380C1C0C0C00000000000000000"
+ "000000003FC07FC0E000C000C000E0007F003F8001C000C000C001C0FF80FF00000000000000000000000000FFC0FFC00C000C000C000C000C000C00"
+ "0C000C000C000C000C000C00000000000000000000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0E1C07F803F000000000000000000"
+ "00000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0E1C0738033001E000C00000000000000000000000000C0C0C0C0C0C0C0C0CCC0CCC0CCC0CCC0"
+ "CCC0CCC0CCC0CCC07F803300000000000000000000000000C0C0C0C0C0C0E1C073803F001E001E003F007380E1C0C0C0C0C0C0C00000000000000000"
+ "00000000C0C0C0C0C0C0C0C0C0C0E1C073803F001E000C000C000C000C000C00000000000000000000000000FFC0FFC000C001C0038007000E001C00"
+ "38007000E000C000FFC0FFC00000000000000000000000003F003F0030003000300030003000300030003000300030003F003F000000000000000000"
+ "0000000000000000C000E000700038001C000E000700038001C000C0000000000000000000000000000000003F003F00030003000300030003000300"
+ "03000300030003003F003F000000000000000000000000000C001E003F007380E1C0C0C0000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000FFC0FFC000000000000000000C000E000700038001C000C000000000"
+ "00000000000000000000000000000000000000000000000000000000000000003F003F8000C000C03FC07FC0C0C0C0C07FC03FC00000000000000000"
+ "00000000C000C000C000C000C000C000DF00FF80F1C0C0C0C0C0C1C0FF80FF0000000000000000000000000000000000000000003F007F80E1C0C0C0"
+ "C000C000C0C0E1C07F803F0000000000000000000000000000C000C000C000C000C000C03EC07FC0E3C0C0C0C0C0E0C07FC03FC00000000000000000"
+ "0000000000000000000000003F007F80C0C0C0C0FF80FF00C000C0007F003F000000000000000000000000000F001F8039C030C030003000FC00FC00"
+ "30003000300030003000300000000000000000000000000000000000000000003FC07FC0E0C0C0C0C0C0E0C07FC03FC000C000C000C001C03F803F00"
+ "00000000C000C000C000C000C000C000DF00FF80E1C0C0C0C0C0C0C0C0C0C0C00000000000000000000000000C000C00000000003C003C000C000C00"
+ "0C000C000C000C003F003F0000000000000000000000000003000300000000000F000F00030003000300030003000300C300C300C300E7007E003C00"
+ "00000000300030003000300030C031C0338037003E003E003700338031C030C00000000000000000000000003C003C000C000C000C000C000C000C00"
+ "0C000C000C000C003F003F000000000000000000000000000000000000000000D300FF80FFC0CCC0CCC0CCC0CCC0CCC0CCC0CCC00000000000000000"
+ "000000000000000000000000CF00FF80F1C0C0C0C0C0C0C0C0C0C0C0C0C0C0C000000000000000000000000000000000000000003F007F80E1C0C0C0"
+ "C0C0C0C0C0C0E1C07F803F000000000000000000000000000000000000000000CF00DF80F9C0F0C0E0C0C1C0FF80FF00C000C000C000C000C000C000"
+ "0000000000000000000000003CC07EC0E7C0C3C0C1C0E0C07FC03FC000C000C000C000C000C000C0000000000000000000000000DF00FF80F1C0C0C0"
+ "C000C000C000C000C000C00000000000000000000000000000000000000000003FC07FC0C000C0007F003F8000C000C0FF80FF000000000000000000"
+ "0000000030003000FC00FC0030003000300030003000300030C039C01F800F000000000000000000000000000000000000000000C0C0C0C0C0C0C0C0"
+ "C0C0C0C0C0C0E3C07FC03CC00000000000000000000000000000000000000000C0C0C0C0C0C0C0C0C0C0E1C0738033001E000C000000000000000000"
+ "000000000000000000000000C0C0C0C0C0C0C0C0C0C0CCC0CCC0CCC07F8033000000000000000000000000000000000000000000C0C0E1C073803F00"
+ "1E001E003F007380E1C0C0C00000000000000000000000000000000000000000C0C0C0C0C0C0C0C0C0C0E0C07FC03FC000C000C000C001C03F803F00"
+ "000000000000000000000000FFC0FFC0030007000E001C0038007000FFC0FFC000000000000000000000000000C001C003800300030006000C000C00"
+ "060003000300038001C000C00000000000000000000000000C000C000C000C000C000C00000000000C000C000C000C000C000C000000000000000000"
+ "00000000300038001C000C000C0006000300030006000C000C001C003800300000000000000000000000000000C001C00F801F003800300000000000"
+ "000000000000000000000000000000000000000000000000FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF0FFF000000000");
// font 6: 7x10, chars 0x20-0xFF, 224 glyphs
private static readonly Face F6 = new(6, 0x20, 0xFF, 7, 10,
"000000000000000000000000000000000000000000001000100010001000000000001000000000000000280028002800000000000000000000000000"
+ "0000280028007C0028007C002800280000000000000010003C0050003800140078001000000000000000600064000800100020004C000C0000000000"
+ "000030004800500020005400480034000000000000003000100020000000000000000000000000000000080010002000200020001000080000000000"
+ "0000200010000800080008001000200000000000000000001000540038005400100000000000000000000000100010007C0010001000000000000000"
+ "000000000000000000000000300010002000000000000000000000007C00000000000000000000000000000000000000000000003000300000000000"
+ "00000000040008001000200040000000000000000000380044004C005400640044003800000000000000100030001000100010001000380000000000"
+ "00003800440004000800100020007C000000000000007C0008001000080004000400780000000000000008001800280048007C000800080000000000"
+ "00007C0040007800040004000400780000000000000018002000400078004400440038000000000000007C0004000800100020002000200000000000"
+ "000038004400440038004400440038000000000000003800440044003C00040008003000000000000000000030003000000030003000000000000000"
+ "000000003000300000003000100020000000000000000800100020004000200010000800000000000000000000007C0000007C000000000000000000"
+ "000040002000100008001000200040000000000000003800440004000800100000001000000000000000380044000400340054005400380000000000"
+ "000038004400440044007C00440044000000000000007800440044007800440044007800000000000000380044004000400040004400380000000000"
+ "000078004400440044004400440078000000000000007C00400040007800400040007C000000000000007C0040004000780040004000400000000000"
+ "00003800440040005C004400440038000000000000004400440044007C00440044004400000000000000380010001000100010001000380000000000"
+ "00001C0008000800080008004800300000000000000044004800500060005000480044000000000000004000400040004000400040007C0000000000"
+ "000044006C005400540044004400440000000000000044004400640054004C0044004400000000000000380044004400440044004400380000000000"
+ "000078004400440078004000400040000000000000003800440044004400540048003400000000000000780044004400780050004800440000000000"
+ "00003C004000400038000400040078000000000000007C00100010001000100010001000000000000000440044004400440044004400380000000000"
+ "000044004400440044004400280010000000000000004400440044005400540054002800000000000000440044002800100028004400440000000000"
+ "000044004400440028001000100010000000000000007C00040008001000200040007C00000000000000380020002000200020002000380000000000"
+ "000040002000100008000400000000000000000000003800080008000800080008003800000000000000100028004400000000000000000000000000"
+ "000000000000000000000000000000000000FE000000300020001000000000000000000000000000000000000000380004003C0044003C0000000000"
+ "0000400040005800640044004400780000000000000000000000380040004000400038000000000000000400040034004C00440044003C0000000000"
+ "000000000000380044007C00400038000000000000001800240020007000200020002000000000000000000000003C00440044003C00040004003800"
+ "000040004000580064004400440044000000000000001000000030001000100010003800000000000000080000001800080008000800080048003000"
+ "000040004000480050006000500048000000000000003000100010001000100010003800000000000000000000006800540054004400440000000000"
+ "000000000000580064004400440044000000000000000000000038004400440044003800000000000000000000005800640044007800400040004000"
+ "00000000000034004C0044003C0004000400040000000000000058006400400040004000000000000000000000003C00400038000400780000000000"
+ "000000002000F8002000200028001000000000000000000000004400440044004C003400000000000000000000004400440044002800100000000000"
+ "000000000000440044005400540028000000000000000000000044002800100028004400000000000000000000004400440044003C00040004003800"
+ "0000000000007C000800100020007C000000000000000800100010002000100010000800000000000000100010001000000010001000100000000000"
+ "00002000100010000800100010002000000000000000040038004000000000000000000000000000FE00FE00FE00FE00FE00FE00FE00FE00FE00FE00"
+ "0000000000000000FE000000000000000000000008000800080008000800080008000800080008000800080008000800F80000000000000000000000"
+ "00000000000000000E000800080008000800080008000800080008000E00000000000000000000000000000000000000F80008000800080008000800"
+ "08000800080008000E00080008000800080008000800080008000800F800080008000800080008000800080008000800FE0008000800080008000800"
+ "0800080008000800FE00000000000000000000000000000000000000FE00080008000800080008000000000000000000020004000800080008000800"
+ "0000000000000000E000100008000800080008000800080008001000E000000000000000000000000800080008000400020000000000000000000000"
+ "1C001C001C00FE00FE00FE001C001C001C001C00000000000000FE00FE00FE0000000000000000001C001C001C001C001C001C001C001C001C001C00"
+ "1C001C001C00FC00FC00FC0000000000000000000000000000001E001E001E001C001C001C001C001C001C001C001E001E001E000000000000000000"
+ "000000000000FC00FC00FC001C001C001C001C001C001C001C001E001E001E001C001C001C001C001C001C001C00FC00FC00FC001C001C001C001C00"
+ "000000001000380054001000100010001000000000000000000020001000F8001000200000000000000000000000080010003E001000080000000000"
+ "00000000000008000800F8000000000000000000000008000800080008002A001C0008000000000000000000080014002200140014001C0000000000"
+ "0000000008001C003E001C001C001C00000000000000000010003E007E003E0010000000000000000000000054004400820082008200440038000000"
+ "000000000000000000007000500070000000000000001C00100010001000000000000000000000000000000000000000100010001000700000000000"
+ "00000000000000000000400020001000000000000000000000000000300030000000000000000000000000007C0004007C0004000800100000000000"
+ "0000000000007C00040018001000200000000000000000000000080010003000500010000000000000000000000010007C0044000400180000000000"
+ "00000000000000007C00100010007C000000000000000000000008007C001800280048000000000000000000000020007C0024002800200000000000"
+ "00000000000000003800080008007C000000000000000000000078000800780008007800000000000000000000000000540054000400180000000000"
+ "0000000000007C0000000000000000000000000000007C00040014001800100010002000000000000000040008001000300050001000100000000000"
+ "000010007C004400440004000800100000000000000000007C0010001000100010007C0000000000000008007C000800180028004800080000000000"
+ "000020007C002400240024002400480000000000000010007C0010007C0010001000100000000000000000003C002400440004000800300000000000"
+ "000020003C004800080008000800100000000000000000007C0004000400040004007C0000000000000028007C002800280008001000200000000000"
+ "0000000060000400640004000800700000000000000000007C000400080010002800440000000000000020007C0024002800200020001C0000000000"
+ "0000000044004400240004000800300000000000000000003C00240054000C00080030000000000000000800700010007C0010001000200000000000"
+ "00000000540054005400040008001000000000000000380000007C001000100010002000000000000000200020002000300028002000200000000000"
+ "0000100010007C0010001000200040000000000000000000380000000000000000007C0000000000000000007C000400280010002800400000000000"
+ "000010007C00080010003800540010000000000000000800080008000800080010002000000000000000000010000800440044004400440000000000"
+ "0000400040007C004000400040003C0000000000000000007C0004000400040008003000000000000000000020005000080004000400000000000000"
+ "000010007C001000100054005400100000000000000000007C0004000400280010000800000000000000000038000000380000003800040000000000"
+ "0000000010002000400044007C000400000000000000000004000400280010002800400000000000000000007C0020007C00200020001C0000000000"
+ "0000200020007C0024002800200020000000000000000000380008000800080008007C0000000000000000007C0004007C00040004007C0000000000"
+ "0000380000007C0004000400080010000000000000004800480048004800080010002000000000000000100050005000500054005400180000000000"
+ "0000400040004000440048005000600000000000000000007C0044004400440044007C0000000000000000007C004400440004000800100000000000"
+ "000000006000000004000400080070000000000000001000480020000000000000000000000000000000700050007000000000000000000000000000"
+ "0000100000001000200040004400380000000000040008003800440044007C004400440000000000400020003800440044007C004400440000000000"
+ "280000003800440044007C0044004400000000000000380044004000400040004400380010003000040008007C0040007800400040007C0000000000"
+ "400020007C0040007800400040007C000000000038004400380010001000100010003800000000002800000038004400440044004400380000000000"
+ "2800000044004400440044004400380000000000040008000000380004003C0044003C0000000000380044000000380004003C0044003C0000000000"
+ "400020000000380004003C0044003C0000000000000028000000380004003C0044003C00000000000000000000003800400040004400380010003000"
+ "040008000000380044007C004000380000000000380044000000380044007C004000380000000000400020000000380044007C004000380000000000"
+ "040008002000000060002000200070000000000038004400100000003000100010003800000000008000400010000000300010001000380000000000"
+ "280000001000000030001000100038000000000034005C00000058006400440044004400000000000400080000003800440044004400380000000000"
+ "380044000000380044004400440038000000000040002000000038004400440044003800000000000000280000003800440044004400380000000000"
+ "0000380044005800440044006400D800000000000800100000004400440044004C003400000000003800440000004400440044004C00340000000000"
+ "4000200000004400440044004C003400000000000000280000004400440044004C00340000000000");
// font 7: 7x10, chars 0x40-0x7F, 64 glyphs
private static readonly Face F7 = new(7, 0x40, 0x7F, 7, 10,
"00000800100020001000080000007C000000000000002000100008001000200000007C00000000000000100010007C001000100000007C0000000000"
+ "00000000100000007C000000100000000000000000001000280028002800440044007C00000000000000040008007C0010007C002000400000000000"
+ "100038005400540054005400540038001000000000007C0020002000200020002000700000000000000000007C0000007C0000007C00000000000000"
+ "0000040038004000040038004000000000000000000004003800400000007C00000000000000000000000400040004004800A8002800100010000000"
+ "0000100028002800280044004400440000000000000038004400040034004C0044004800300000000000000000004400280010002800440000000000"
+ "00000000100038007C0038001000000000000000000010003800540050005400380010000000000000007C0044004400440044004400440000000000"
+ "0000380044004400540044004400380000000000000000000000280054002800000000000000000000007C00200010000800100020007C0000000000"
+ "00001800240024001800000000000000000000000000380044009200BA0092004400380000000000000038004400AA009200AA004400380000000000"
+ "00000000380044004400440028006C000000000000007C00000000003800000000007C00000000000000540054005400380010001000100000000000"
+ "00003A0044008A009200A2004400B800000000000000380044009A00A2009A00440038000000000000007A002E002A002A0000000000000000000000"
+ "08001C002A00080008000800080008000800080008000800080008000800080008002A001C0008000000180024002000780020002400580000000000"
+ "000000000000340048004800340000000000000000003800440044007800440064004C00400000000000000040004400280010002800440004000000"
+ "000038004400200010002800440044003800000000000000000038004400300044003800000000000000000010003800540054005400380010000000"
+ "000000000000440044002800280038001000100000000000000058006400440044004400040000000000000000000000300010001000180000000000"
+ "000000000000080054005400540038001000000000000000000024002800300028002400000000000000600020001000100028004400440000000000"
+ "000000000000440044006400580040004000000000000000000044004400280028001000000000000000000000003800440044004400380000000000"
+ "00004400280010007C0010007C001000000000000000000000007C0028002800280024000000000000003800440044007C0044004400380000000000"
+ "00000000000038004400640058004000400000000000000000003C004800440044003800000000000000000004003800500010001000080000000000"
+ "00000000000040002800240024001800000000000000000000007C002800440054002800000000000000000000000000280044005400280000000000"
+ "00002000180020001800200020001C0004000000000000001000540054003800100010000000000000002000180020002000200020001C0004000000"
+ "00000000080004007E00040008000000000000000000000020004000FE00400020000000000000000000000028004400FE0044002800000000000000"
+ "08001C002A0008000800080008002A001C000800");
/// <summary>All 8 faces, indexed 0-7 (matches ESC K).</summary>
public static readonly Face[] All = { F0, F1, F2, F3, F4, F5, F6, F7 };
/// <summary>The default/power-on font (font 0: 6x8, full 0x20-0xFF range).</summary>
public static Face Default => F0;
}
+3 -2
View File
@@ -50,10 +50,11 @@ public static class PlasmaSelfTest
b.Add(PlasmaProtocol.LineFeed);
Text(b, "128x32 PLASMA DISPLAY"); // exactly one 21-cell row
// Attributes are the low 4 bits: 1=half, 2=underline, 4=reverse, 8=flash.
Esc(b, PlasmaProtocol.CmdAttributes, 1); Text(b, "HALF ");
Esc(b, PlasmaProtocol.CmdAttributes, 2); Text(b, "UNDER ");
Esc(b, PlasmaProtocol.CmdAttributes, 3); Text(b, "REV");
Esc(b, PlasmaProtocol.CmdAttributes, 4); Text(b, " FLASH");
Esc(b, PlasmaProtocol.CmdAttributes, 4); Text(b, "REV");
Esc(b, PlasmaProtocol.CmdAttributes, 8); Text(b, " FLASH");
Esc(b, PlasmaProtocol.CmdAttributes, PlasmaProtocol.OperandDefault);
return b.ToArray();
}
+104 -85
View File
@@ -10,7 +10,7 @@ public enum PlasmaCursorMode
Flashing,
}
/// <summary>Text rendering attributes (set with <c>ESC H</c>).</summary>
/// <summary>Text rendering attributes (set with <c>ESC H</c>, low 4 bits).</summary>
[Flags]
public enum PlasmaAttributes : byte
{
@@ -27,17 +27,23 @@ public enum PlasmaAttributes : byte
/// COM2. Feed raw wire bytes to <see cref="OnReceived"/>; the parser is a
/// state machine, so commands may arrive split across any chunk boundaries.
///
/// <para>Grounded in the real firmware (<c>PlasmaNew/FIRMWARE.md</c>): the
/// cursor is a <b>pixel</b> position — <c>ESC Q</c> sets its row (Y, 031),
/// <c>ESC R</c> its column (X, 0127) — and glyphs are drawn there and advance
/// X by the font's width. The eight fonts are the real ROM character generator
/// (<see cref="PlasmaFonts"/>): 6×8, 6×10, 7×10, and the large 12×16 / 12×20,
/// selected with <c>ESC K</c> (07). Attributes are the low 4 bits of the
/// <c>ESC H</c> operand.</para>
///
/// <para>Pixels carry flags rather than a plain bit: graphics writes set
/// full-intensity dots, while text can stamp half-intensity or flashing dots
/// (<c>ESC H</c>); the UI renders <see cref="PixelHalf"/> dimmer and blinks
/// <see cref="PixelFlash"/>. The command set itself is documented on
/// full-intensity dots, while text can stamp half-intensity or flashing dots;
/// the UI renders <see cref="PixelHalf"/> dimmer and blinks
/// <see cref="PixelFlash"/>. The command set is documented on
/// <see cref="PlasmaProtocol"/>.</para>
///
/// <para>Grid geometry: fonts 03 are the 5×7 set in a 6×8 cell (21 columns
/// × 4 rows), fonts 47 the same glyphs doubled into a 12×16 cell
/// (10 × 2). Which of the eight slots the real panel mapped to which face is
/// lost with the hardware; two sizes cover what the surviving software
/// exercises.</para>
/// <para>Not yet folded in from the firmware (documented, deferred): the 10
/// double-buffered pages (<c>ESC I</c>/<c>ESC i</c>) and the vector-graphics
/// primitives (<c>ESC A</c><c>F</c>). vPLASMA models a single page.</para>
///
/// <para>Thread-safe: the serial reader feeds bytes while the UI snapshots
/// frames. Events are raised outside the lock, on the caller's thread.</para>
@@ -58,8 +64,9 @@ public sealed class VPlasmaDevice
// ---- text-mode state -------------------------------------------------
private int _font; // 0..7
private PlasmaFonts.Face _face = PlasmaFonts.Default;
private PlasmaAttributes _attributes;
private int _col, _row; // cursor, in cells of the current grid
private int _x, _y; // cursor, in pixels (0..127, 0..31)
private PlasmaCursorMode _cursorMode = PlasmaCursorMode.Steady; // power-on default; the game hides it
// ---- parser state ----------------------------------------------------
@@ -99,16 +106,14 @@ public sealed class VPlasmaDevice
public int Font { get { lock (_sync) return _font; } }
public PlasmaAttributes Attributes { get { lock (_sync) return _attributes; } }
// Current font grid, for the UI's cursor overlay and status line.
private int FontScale => _font >= 4 ? 2 : 1;
public int CellWidth { get { lock (_sync) return 6 * FontScale; } }
public int CellHeight { get { lock (_sync) return 8 * FontScale; } }
private int Columns => Width / (6 * FontScale);
private int Rows => Height / (8 * FontScale);
public Point CursorCell { get { lock (_sync) return new Point(_col, _row); } }
/// <summary>A cursor cell position (avoids dragging in System.Drawing).</summary>
public readonly record struct Point(int Col, int Row);
/// <summary>Cursor column X in pixels (0127).</summary>
public int CursorX { get { lock (_sync) return _x; } }
/// <summary>Cursor row Y in pixels (031).</summary>
public int CursorY { get { lock (_sync) return _y; } }
/// <summary>Current font cell width in pixels.</summary>
public int FontWidth { get { lock (_sync) return _face.Width; } }
/// <summary>Current font cell height in pixels.</summary>
public int FontHeight { get { lock (_sync) return _face.Height; } }
/// <summary>Copy the frame into <paramref name="destination"/> (Width*Height flag bytes).</summary>
public void CopyFrame(byte[] destination)
@@ -125,8 +130,9 @@ public sealed class VPlasmaDevice
lock (_sync)
{
Array.Clear(_pixels, 0, _pixels.Length);
_col = _row = 0;
_x = _y = 0;
_font = 0;
_face = PlasmaFonts.Default;
_attributes = PlasmaAttributes.None;
_cursorMode = PlasmaCursorMode.Steady;
_state = State.Text;
@@ -187,7 +193,7 @@ public sealed class VPlasmaDevice
return;
case PlasmaProtocol.BackSpace:
if (_col > 0) _col--;
_x = Math.Max(0, _x - _face.Width);
_dirty = true;
return;
@@ -197,17 +203,18 @@ public sealed class VPlasmaDevice
return;
case PlasmaProtocol.LineFeed:
_row = (_row + 1) % Rows;
NextLine();
_dirty = true;
return;
case PlasmaProtocol.VerticalTab:
_row = (_row + Rows - 1) % Rows;
_y -= _face.Height;
if (_y < 0) _y = Math.Max(0, Height - _face.Height);
_dirty = true;
return;
case PlasmaProtocol.CarriageReturn:
_col = 0;
_x = 0;
_dirty = true;
return;
}
@@ -232,8 +239,9 @@ public sealed class VPlasmaDevice
{
case PlasmaProtocol.CmdClearScreen:
Array.Clear(_pixels, 0, _pixels.Length);
_col = _row = 0;
_x = _y = 0;
_font = 0;
_face = PlasmaFonts.Default;
_attributes = PlasmaAttributes.None;
_dirty = true;
Log("Clear screen (ESC @)");
@@ -241,7 +249,7 @@ public sealed class VPlasmaDevice
break;
case PlasmaProtocol.CmdHomeCursor:
_col = _row = 0;
_x = _y = 0;
_dirty = true;
Log("Home cursor (ESC L)");
_graphicsLogArmed = true;
@@ -250,6 +258,8 @@ public sealed class VPlasmaDevice
case PlasmaProtocol.CmdCursorMode:
case PlasmaProtocol.CmdFontSelect:
case PlasmaProtocol.CmdAttributes:
case PlasmaProtocol.CmdSetRow:
case PlasmaProtocol.CmdSetColumn:
_pendingCommand = b;
_state = State.Operand;
break;
@@ -282,50 +292,52 @@ public sealed class VPlasmaDevice
break;
case PlasmaProtocol.CmdFontSelect:
_font = operand == PlasmaProtocol.OperandDefault ? 0 : operand & 0x07;
// The cursor keeps its cell coordinates but the grid changed size.
_col = Math.Min(_col, Columns - 1);
_row = Math.Min(_row, Rows - 1);
_dirty = true;
Log($"Font {_font}: {Columns}×{Rows} cells (ESC K {operand:X2})");
// Firmware: operands 07 select a real font (8 fonts); it range-
// checks and ignores anything larger, so ESC K FF is a no-op.
if (operand < PlasmaFonts.All.Length)
{
_font = operand;
_face = PlasmaFonts.All[operand];
_x = Math.Min(_x, Width - 1);
_y = Math.Min(_y, Height - 1);
_dirty = true;
Log($"Font {_font}: {_face.Width}×{_face.Height} (ESC K {operand:X2})");
}
else
{
Log($"Font select ignored (ESC K {operand:X2} out of range)");
}
break;
case PlasmaProtocol.CmdAttributes:
_attributes = DecodeAttributes(operand);
// Firmware stores the low 4 bits directly as flags.
_attributes = (PlasmaAttributes)(operand & 0x0F);
Log($"Attributes {(_attributes == PlasmaAttributes.None ? "default" : _attributes.ToString())} (ESC H {operand:X2})");
break;
case PlasmaProtocol.CmdSetRow:
// ESC Q: set cursor row Y in pixels; firmware range-checks 031.
if (operand < Height)
{
_y = operand;
_dirty = true;
Log($"Cursor row Y={operand} (ESC Q {operand:X2})");
}
break;
case PlasmaProtocol.CmdSetColumn:
// ESC R: set cursor column X in pixels; firmware range-checks 0127.
if (operand < Width)
{
_x = operand;
_dirty = true;
Log($"Cursor col X={operand} (ESC R {operand:X2})");
}
break;
}
_graphicsLogArmed = true;
}
/// <summary>
/// <c>ESC H</c>'s operand indexes the style list PLASMA.EXE enumerates
/// (its <c>/s</c> option): the 17 intensity/underline/reverse/flash
/// combos below, in the tool's own order. FF (and anything out of range)
/// restores the defaults.
/// </summary>
private static PlasmaAttributes DecodeAttributes(byte operand) => operand switch
{
0 => PlasmaAttributes.None,
1 => PlasmaAttributes.HalfIntensity,
2 => PlasmaAttributes.Underline,
3 => PlasmaAttributes.Reverse,
4 => PlasmaAttributes.Flash,
5 => PlasmaAttributes.Underline,
6 => PlasmaAttributes.Reverse,
7 => PlasmaAttributes.Flash,
8 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline,
9 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Reverse,
10 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Flash,
11 => PlasmaAttributes.Underline | PlasmaAttributes.Reverse,
12 => PlasmaAttributes.Underline | PlasmaAttributes.Flash,
13 => PlasmaAttributes.Underline | PlasmaAttributes.Reverse | PlasmaAttributes.Flash,
14 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline | PlasmaAttributes.Reverse,
15 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline | PlasmaAttributes.Flash,
16 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline | PlasmaAttributes.Reverse | PlasmaAttributes.Flash,
_ => PlasmaAttributes.None,
};
// ---- graphics writes (ESC P) -------------------------------------------
private void BeginGraphicsData()
@@ -374,34 +386,35 @@ public sealed class VPlasmaDevice
private void DrawChar(byte code)
{
int scale = FontScale;
int cellW = 6 * scale, cellH = 8 * scale;
int ox = _col * cellW, oy = _row * cellH;
Span<byte> columns = stackalloc byte[PlasmaFont.GlyphWidth];
PlasmaFont.GetColumns(code, columns);
// The firmware ignores a character outside the current font's range.
if (!_face.Has(code))
return;
int w = _face.Width, h = _face.Height;
bool reverse = (_attributes & PlasmaAttributes.Reverse) != 0;
bool underline = (_attributes & PlasmaAttributes.Underline) != 0;
byte litFlags = PixelLit;
if ((_attributes & PlasmaAttributes.HalfIntensity) != 0) litFlags |= PixelHalf;
if ((_attributes & PlasmaAttributes.Flash) != 0) litFlags |= PixelFlash;
for (int cy = 0; cy < cellH; ++cy)
for (int row = 0; row < h; ++row)
{
int glyphRow = cy / scale; // 0..7; row 7 is the gap/underline row
int rowOffset = (oy + cy) * Width + ox;
for (int cx = 0; cx < cellW; ++cx)
int py = _y + row;
if ((uint)py >= Height)
continue;
ushort bits = _face.Row(code, row); // bit 15 = leftmost pixel
int rowOffset = py * Width;
for (int col = 0; col < w; ++col)
{
int glyphCol = cx / scale; // 0..5; column 5 is the gap column
bool on = glyphCol < PlasmaFont.GlyphWidth
&& glyphRow < PlasmaFont.GlyphHeight
&& (columns[glyphCol] >> glyphRow & 1) != 0;
if (underline && glyphRow == 7)
int px = _x + col;
if ((uint)px >= Width)
continue;
bool on = (bits & (0x8000 >> col)) != 0;
if (underline && row == h - 1)
on = true;
if (reverse)
on = !on;
_pixels[rowOffset + cx] = on ? litFlags : (byte)0;
_pixels[rowOffset + px] = on ? litFlags : (byte)0;
}
}
@@ -410,15 +423,21 @@ public sealed class VPlasmaDevice
AdvanceCursor();
}
/// <summary>Advance the cursor one cell, wrapping at the right/bottom edges.</summary>
private void AdvanceCursor()
{
if (++_col >= Columns)
{
_col = 0;
// No scroll on these panels: writing past the last row wraps to the top.
if (++_row >= Rows)
_row = 0;
}
_x += _face.Width;
if (_x > Width - _face.Width)
NextLine();
}
private void NextLine()
{
_x = 0;
_y += _face.Height;
// No scroll on these panels: past the last line wraps to the top.
if (_y > Height - _face.Height)
_y = 0;
}
// ---- event plumbing --------------------------------------------------------
@@ -58,6 +58,11 @@ public static class PlasmaProtocol
public const byte CmdFontSelect = (byte)'K';
public const byte CmdHomeCursor = (byte)'L';
public const byte CmdGraphicsWrite = (byte)'P';
// Cursor positioning, confirmed from the firmware dump (FIRMWARE.md): the
// cursor is a pixel position, ESC Q sets its row (Y, 0-31) and ESC R its
// column (X, 0-127) — the exact range checks the firmware enforces.
public const byte CmdSetRow = (byte)'Q';
public const byte CmdSetColumn = (byte)'R';
/// <summary>Operand meaning "restore the default" for ESC K / ESC H.</summary>
public const byte OperandDefault = 0xFF;