From 49e88fbe20ba8a863f299f2355459f32af74959f Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 16 Jul 2026 17:39:07 -0500 Subject: [PATCH] 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 --- PlasmaNew/FIRMWARE.md | 12 +- README.md | 17 +- src/VPlasma.App/PlasmaCanvas.cs | 13 +- src/VPlasma.Core/Device/PlasmaFont.cs | 130 ----- src/VPlasma.Core/Device/PlasmaFonts.cs | 450 ++++++++++++++++++ src/VPlasma.Core/Device/PlasmaSelfTest.cs | 5 +- src/VPlasma.Core/Device/VPlasmaDevice.cs | 189 ++++---- src/VPlasma.Core/Protocol/PlasmaProtocol.cs | 5 + .../VPlasma.Core.Tests/VPlasmaDeviceTests.cs | 135 +++--- 9 files changed, 663 insertions(+), 293 deletions(-) delete mode 100644 src/VPlasma.Core/Device/PlasmaFont.cs create mode 100644 src/VPlasma.Core/Device/PlasmaFonts.cs diff --git a/PlasmaNew/FIRMWARE.md b/PlasmaNew/FIRMWARE.md index 2b67fc5..25fa481 100644 --- a/PlasmaNew/FIRMWARE.md +++ b/PlasmaNew/FIRMWARE.md @@ -127,10 +127,14 @@ at `$8000`). It's how the new `ESC R/I/Q/i/K` commands were first spotted, e.g. ## What this means -**For vPLASMA:** the recovered commands can replace guessed behavior. Notably -`ESC Q`/`ESC R` are the real cursor-positioning commands (row/col), and the -`ESC P` "screen" byte selects one of 10 double-buffered pages — vPLASMA -currently models a single buffer. +**For vPLASMA (folded in 2026-07-16):** the recovered spec replaced the +guessed behavior. vPLASMA now uses the **8 real ROM fonts** (extracted to +`src/VPlasma.Core/Device/PlasmaFonts.cs`), a **pixel-addressed cursor** with +the real `ESC Q` (row) / `ESC R` (column) positioning, `ESC K` 0–7 font +select, and `ESC H` attributes as the low 4 bits. Verified: 24 unit tests + +the three self-test pages render the real glyphs. Still deferred (documented, +single-page model retained): the 10 double-buffered pages (`ESC I`/`ESC i`) +and the vector-graphics primitives (`ESC A`–`F`). **For the replica:** this *is* the spec. The firmware confirms a clean model — a byte-stream command parser, a 512-byte-per-page frame buffer, 10 pages with diff --git a/README.md b/README.md index e4c3e3c..309a730 100644 --- a/README.md +++ b/README.md @@ -157,12 +157,15 @@ recovered grammar lives in `src/VPlasma.Core/Protocol/PlasmaProtocol.cs`. - **Graphics** — `ESC P screen y xbyte width rows` + packed 1bpp row data, MSB = leftmost pixel. This is everything the game sends, so it is the wire path a pod's plasma actually sees. -- **Text** — printable ASCII renders through a 5×7 font at a cursor: - fonts 0–3 give a 21×4 cell grid, fonts 4–7 the same glyphs doubled to - 10×2. Half-intensity draws dimmer, reverse/underline render in the - cell, flashing text (and the flashing cursor) blink on the glass. The - panel's own ROM glyphs are lost with the hardware, so the classic - public-domain 5×7 set stands in. +- **Text** — printable ASCII renders at a **pixel-addressed cursor** + (`ESC Q` sets row Y 0–31, `ESC R` sets column X 0–127) through the + **real ROM character generator** — the eight Babcock fonts recovered + from the firmware dump (`ESC K` 0–7): 6×8, 6×10, 7×10, and the large + 12×16 / 12×20. Attributes (`ESC H`, low 4 bits) — half-intensity + draws dimmer, underline/reverse render in the cell, flashing text (and + the flashing cursor) blink on the glass. Extracted from the U3 EPROM; + see [`PlasmaNew/FIRMWARE.md`](PlasmaNew/FIRMWARE.md). (Deferred, and + documented there: the 10 double-buffered pages and vector graphics.) - **Double-click** the glass to cycle three self-test pages (banner, charset, graphics pattern) through the same parser the wire feeds — useful without a host. **Right-click** resets the display to its @@ -178,7 +181,7 @@ recovered grammar lives in `src/VPlasma.Core/Protocol/PlasmaProtocol.cs`. |------|----------| | `src/VRio.Core` | Protocol framing/builder/parser, the `VRioDevice` state machine, serial pump, panel layout data (class library) | | `src/VRio.App` | WinForms panel UI | -| `src/VPlasma.Core` | Plasma command-stream parser, the `VPlasmaDevice` display state machine, 5×7 font, serial listener (class library) | +| `src/VPlasma.Core` | Plasma command-stream parser, the `VPlasmaDevice` display state machine, the 8 real ROM fonts (`PlasmaFonts`), serial listener (class library) | | `src/VPlasma.App` | WinForms dot-matrix display UI | | `pkg` | Sparse-package manifest + registration script: grants VRio.App.exe the package identity Dynamic Lighting's background-control list requires | | `tests/VRio.Core.Tests` | xUnit tests for the protocol + device engine | diff --git a/src/VPlasma.App/PlasmaCanvas.cs b/src/VPlasma.App/PlasmaCanvas.cs index f42b393..bee6c42 100644 --- a/src/VPlasma.App/PlasmaCanvas.cs +++ b/src/VPlasma.App/PlasmaCanvas.cs @@ -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); diff --git a/src/VPlasma.Core/Device/PlasmaFont.cs b/src/VPlasma.Core/Device/PlasmaFont.cs deleted file mode 100644 index c88f2be..0000000 --- a/src/VPlasma.Core/Device/PlasmaFont.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace VPlasma.Core.Device; - -/// -/// 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. -/// -public static class PlasmaFont -{ - public const int GlyphWidth = 5; - public const int GlyphHeight = 7; - public const byte First = 0x20; - public const byte Last = 0x7E; - - /// - /// Column bits for 's glyph. Bit r of - /// result[c] is the dot at column c, row r (row 0 at the top). - /// - public static void GetColumns(byte code, Span 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 '~' - }; -} diff --git a/src/VPlasma.Core/Device/PlasmaFonts.cs b/src/VPlasma.Core/Device/PlasmaFonts.cs new file mode 100644 index 0000000..03e6bd9 --- /dev/null +++ b/src/VPlasma.Core/Device/PlasmaFonts.cs @@ -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; + +/// The display's 8 built-in fonts, extracted from ROM. +public static class PlasmaFonts +{ + /// One font face: character range, cell size, and packed glyph rows. + 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; + /// True if this face has a glyph for . + public bool Has(byte code) => code >= First && code <= Last; + /// Row of glyph (bit15 = leftmost), or 0. + 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"); + + /// All 8 faces, indexed 0-7 (matches ESC K). + public static readonly Face[] All = { F0, F1, F2, F3, F4, F5, F6, F7 }; + + /// The default/power-on font (font 0: 6x8, full 0x20-0xFF range). + public static Face Default => F0; +} diff --git a/src/VPlasma.Core/Device/PlasmaSelfTest.cs b/src/VPlasma.Core/Device/PlasmaSelfTest.cs index 584efb6..f49f5a1 100644 --- a/src/VPlasma.Core/Device/PlasmaSelfTest.cs +++ b/src/VPlasma.Core/Device/PlasmaSelfTest.cs @@ -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(); } diff --git a/src/VPlasma.Core/Device/VPlasmaDevice.cs b/src/VPlasma.Core/Device/VPlasmaDevice.cs index ff1c0e0..6a567d9 100644 --- a/src/VPlasma.Core/Device/VPlasmaDevice.cs +++ b/src/VPlasma.Core/Device/VPlasmaDevice.cs @@ -10,7 +10,7 @@ public enum PlasmaCursorMode Flashing, } -/// Text rendering attributes (set with ESC H). +/// Text rendering attributes (set with ESC H, low 4 bits). [Flags] public enum PlasmaAttributes : byte { @@ -27,17 +27,23 @@ public enum PlasmaAttributes : byte /// COM2. Feed raw wire bytes to ; the parser is a /// state machine, so commands may arrive split across any chunk boundaries. /// +/// Grounded in the real firmware (PlasmaNew/FIRMWARE.md): the +/// cursor is a pixel position — ESC Q sets its row (Y, 0–31), +/// ESC R its column (X, 0–127) — and glyphs are drawn there and advance +/// X by the font's width. The eight fonts are the real ROM character generator +/// (): 6×8, 6×10, 7×10, and the large 12×16 / 12×20, +/// selected with ESC K (0–7). Attributes are the low 4 bits of the +/// ESC H operand. +/// /// Pixels carry flags rather than a plain bit: graphics writes set -/// full-intensity dots, while text can stamp half-intensity or flashing dots -/// (ESC H); the UI renders dimmer and blinks -/// . The command set itself is documented on +/// full-intensity dots, while text can stamp half-intensity or flashing dots; +/// the UI renders dimmer and blinks +/// . The command set is documented on /// . /// -/// Grid geometry: fonts 0–3 are the 5×7 set in a 6×8 cell (21 columns -/// × 4 rows), fonts 4–7 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. +/// Not yet folded in from the firmware (documented, deferred): the 10 +/// double-buffered pages (ESC I/ESC i) and the vector-graphics +/// primitives (ESC AF). vPLASMA models a single page. /// /// Thread-safe: the serial reader feeds bytes while the UI snapshots /// frames. Events are raised outside the lock, on the caller's thread. @@ -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); } } - - /// A cursor cell position (avoids dragging in System.Drawing). - public readonly record struct Point(int Col, int Row); + /// Cursor column X in pixels (0–127). + public int CursorX { get { lock (_sync) return _x; } } + /// Cursor row Y in pixels (0–31). + public int CursorY { get { lock (_sync) return _y; } } + /// Current font cell width in pixels. + public int FontWidth { get { lock (_sync) return _face.Width; } } + /// Current font cell height in pixels. + public int FontHeight { get { lock (_sync) return _face.Height; } } /// Copy the frame into (Width*Height flag bytes). 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 0–7 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 0–31. + 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 0–127. + if (operand < Width) + { + _x = operand; + _dirty = true; + Log($"Cursor col X={operand} (ESC R {operand:X2})"); + } + break; } _graphicsLogArmed = true; } - /// - /// ESC H's operand indexes the style list PLASMA.EXE enumerates - /// (its /s option): the 17 intensity/underline/reverse/flash - /// combos below, in the tool's own order. FF (and anything out of range) - /// restores the defaults. - /// - 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 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(); } + /// Advance the cursor one cell, wrapping at the right/bottom edges. 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 -------------------------------------------------------- diff --git a/src/VPlasma.Core/Protocol/PlasmaProtocol.cs b/src/VPlasma.Core/Protocol/PlasmaProtocol.cs index 2fa1be0..3e9a690 100644 --- a/src/VPlasma.Core/Protocol/PlasmaProtocol.cs +++ b/src/VPlasma.Core/Protocol/PlasmaProtocol.cs @@ -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'; /// Operand meaning "restore the default" for ESC K / ESC H. public const byte OperandDefault = 0xFF; diff --git a/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs b/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs index 891a62b..4bd1e90 100644 --- a/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs +++ b/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs @@ -53,7 +53,6 @@ public class VPlasmaDeviceTests { byte[] wire = GraphicsRow(3, Enumerable.Repeat((byte)0xFF, 16).ToArray()); - // Split the same command at every possible boundary. for (int split = 1; split < wire.Length; ++split) { var device = new VPlasmaDevice(); @@ -71,7 +70,6 @@ public class VPlasmaDeviceTests public void GraphicsBlock_MultipleRowsAdvanceY() { var device = new VPlasmaDevice(); - // screen 0, y=10, xbyte=0, 1 byte/row, 3 rows. Feed(device, Esc, (byte)'P', 0, 10, 0, 1, 3, 0x80, 0x80, 0x80); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 10)); @@ -98,10 +96,8 @@ public class VPlasmaDeviceTests { var device = new VPlasmaDevice(); Feed(device, GraphicsRow(40, Enumerable.Repeat((byte)0xFF, 16).ToArray())); - Feed(device, (byte)'!'); // parser must be back in text mode + Feed(device, (byte)'H'); // parser must be back in text mode - var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height]; - device.CopyFrame(frame); Assert.Equal(1, device.TextCharsDrawn); Assert.Equal(0, device.GraphicsRows); } @@ -110,64 +106,83 @@ public class VPlasmaDeviceTests public void GraphicsWrite_OverwritesTextAttributes() { var device = new VPlasmaDevice(); - Feed(device, Esc, (byte)'H', 4); // flashing text - Feed(device, (byte)'H'); // glyph col 0 is a full bar at x=0 + Feed(device, Esc, (byte)'H', 8); // flashing text + Feed(device, (byte)'H'); // font-0 'H' lights (0,0) Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelFlash, Pixel(device, 0, 0)); Feed(device, GraphicsRow(0, 0x80)); // repaint that row from the wire Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); } - // ---- text mode --------------------------------------------------------- + // ---- text mode (real font 0 = 6×8) ----------------------------------- [Fact] - public void Text_DrawsGlyphAndAdvancesCursor() + public void Text_DrawsRealGlyphAndAdvancesByFontWidth() { var device = new VPlasmaDevice(); - Feed(device, (byte)'H'); // 5×7 'H': column 0 = 0x7F (all seven rows) + Feed(device, (byte)'H'); // font-0 'H': row0 = pixels at x0 and x4 Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 6)); - Assert.Equal(0, Pixel(device, 0, 7)); // gap row - Assert.Equal(0, Pixel(device, 5, 0)); // gap column - Assert.Equal(new VPlasmaDevice.Point(1, 0), device.CursorCell); + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 4, 0)); + Assert.Equal(0, Pixel(device, 1, 0)); + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3)); // crossbar row + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 4, 3)); + Assert.Equal(0, Pixel(device, 0, 7)); // blank bottom row + Assert.Equal(6, device.CursorX); // advanced one 6-px cell + Assert.Equal(0, device.CursorY); Assert.Equal(1, device.TextCharsDrawn); } [Fact] - public void ControlChars_MoveTheCursor() + public void ControlChars_MoveCursorByPixels() { - var device = new VPlasmaDevice(); + var device = new VPlasmaDevice(); // font 0: 6 wide, 8 tall - Feed(device, 0x09, 0x09, 0x09); // HT ×3 - Assert.Equal(new VPlasmaDevice.Point(3, 0), device.CursorCell); + Feed(device, 0x09, 0x09, 0x09); // HT ×3 → x = 18 + Assert.Equal(18, device.CursorX); - Feed(device, 0x08); // BS - Assert.Equal(new VPlasmaDevice.Point(2, 0), device.CursorCell); + Feed(device, 0x08); // BS → x = 12 + Assert.Equal(12, device.CursorX); - Feed(device, 0x0A); // LF - Assert.Equal(new VPlasmaDevice.Point(2, 1), device.CursorCell); + Feed(device, 0x0A); // LF → y = 8 + Assert.Equal(8, device.CursorY); + Assert.Equal(0, device.CursorX); - Feed(device, 0x0D); // CR - Assert.Equal(new VPlasmaDevice.Point(0, 1), device.CursorCell); + Feed(device, (byte)'H', 0x0D); // draw + CR → x back to 0 + Assert.Equal(0, device.CursorX); - Feed(device, 0x0B); // VT - Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); - - Feed(device, 0x0B); // VT off the top wraps to the bottom row - Assert.Equal(new VPlasmaDevice.Point(0, 3), device.CursorCell); + Feed(device, 0x0B); // VT → y back to 0 + Assert.Equal(0, device.CursorY); } [Fact] - public void Text_WrapsAtRowAndScreenEnd() + public void EscQ_EscR_SetPixelCursor() { var device = new VPlasmaDevice(); - Feed(device, Enumerable.Repeat((byte)'X', 22)); // one full 21-cell row + 1 - Assert.Equal(new VPlasmaDevice.Point(1, 1), device.CursorCell); + Feed(device, Esc, (byte)'Q', 20); // set row Y=20 + Feed(device, Esc, (byte)'R', 37); // set col X=37 + Assert.Equal(37, device.CursorX); + Assert.Equal(20, device.CursorY); - Feed(device, Enumerable.Repeat((byte)'X', 21 * 3 - 1)); // exactly to the end - Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); // wrapped to top + Feed(device, Esc, (byte)'Q', 40); // out of range (>31): ignored + Assert.Equal(20, device.CursorY); + Feed(device, Esc, (byte)'R', 200); // out of range (>127): ignored + Assert.Equal(37, device.CursorX); + } + + [Fact] + public void Text_WrapsAtRightAndBottom() + { + var device = new VPlasmaDevice(); // 21 cells of 6px per line, 4 lines + + Feed(device, Enumerable.Repeat((byte)'X', 21)); // fills first line, wraps + Assert.Equal(0, device.CursorX); + Assert.Equal(8, device.CursorY); + + Feed(device, Enumerable.Repeat((byte)'X', 21 * 3)); // to the end, wraps to top + Assert.Equal(0, device.CursorX); + Assert.Equal(0, device.CursorY); } [Fact] @@ -178,7 +193,8 @@ public class VPlasmaDeviceTests Feed(device, Esc, (byte)'@'); Assert.Equal(0, Pixel(device, 0, 0)); - Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); + Assert.Equal(0, device.CursorX); + Assert.Equal(0, device.CursorY); Assert.Equal(0, device.Font); Assert.Equal(PlasmaAttributes.None, device.Attributes); } @@ -189,7 +205,8 @@ public class VPlasmaDeviceTests var device = new VPlasmaDevice(); Feed(device, (byte)'H', Esc, (byte)'L'); - Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); + Assert.Equal(0, device.CursorX); + Assert.Equal(0, device.CursorY); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); // glyph survives } @@ -206,42 +223,42 @@ public class VPlasmaDeviceTests } [Fact] - public void EscK_SelectsFontGrids() + public void EscK_SelectsRealFonts() { var device = new VPlasmaDevice(); - Feed(device, Esc, (byte)'K', 4); // large: 12×16 cells + Feed(device, Esc, (byte)'K', 4); // large font: 12×16 Assert.Equal(4, device.Font); - Assert.Equal(12, device.CellWidth); - Assert.Equal(16, device.CellHeight); + Assert.Equal(12, device.FontWidth); + Assert.Equal(16, device.FontHeight); - Feed(device, (byte)'A'); // large glyph: 'A' col 1 (0x11) row 0 → 2×2 dots at (2..3, 0..1) - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 2, 0)); - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 3, 1)); - - Feed(device, Esc, (byte)'K', 0xFF); // FF → default font 0 + Feed(device, Esc, (byte)'K', 0); // back to font 0: 6×8 + Assert.Equal(0, device.Font); + Assert.Equal(6, device.FontWidth); + Assert.Equal(8, device.FontHeight); + + Feed(device, Esc, (byte)'K', 0xFF); // out of range: firmware ignores it Assert.Equal(0, device.Font); - Assert.Equal(6, device.CellWidth); } [Fact] - public void EscH_AppliesAttributes() + public void EscH_AppliesAttributesAsLowFourBits() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'H', 1, (byte)'H'); // half intensity Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelHalf, Pixel(device, 0, 0)); - Feed(device, Esc, (byte)'H', 2, (byte)'H'); // underline: gap row lit - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 6, 7)); + Feed(device, Esc, (byte)'L', Esc, (byte)'@'); // clear + Feed(device, Esc, (byte)'H', 2, (byte)'H'); // underline: bottom row (y=7) lit + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 7)); - Feed(device, Esc, (byte)'H', 3, (byte)' '); // reverse: a space renders solid - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 12, 0)); - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 17, 7)); + Feed(device, Esc, (byte)'@'); + Feed(device, Esc, (byte)'H', 4, (byte)' '); // reverse: a space renders solid + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); + Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 5, 7)); - Feed(device, Esc, (byte)'H', 0xFF, (byte)'H'); // defaults restored - Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 18, 0)); - Assert.Equal(0, Pixel(device, 18, 7)); + Feed(device, Esc, (byte)'H', 0); // defaults restored Assert.Equal(PlasmaAttributes.None, device.Attributes); } @@ -251,7 +268,7 @@ public class VPlasmaDeviceTests public void UnknownEscape_IsConsumedAndTextResumes() { var device = new VPlasmaDevice(); - Feed(device, Esc, (byte)'Z', (byte)'H'); + Feed(device, Esc, (byte)'\\', (byte)'H'); // ESC '\' is not a command Assert.Equal(1, device.TextCharsDrawn); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); @@ -260,11 +277,10 @@ public class VPlasmaDeviceTests [Fact] public void GameStartupSequence_HidesCursor() { - // L4PLASMA.CPP's constructor sends exactly ESC 'G' 0x00. var device = new VPlasmaDevice(); Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); // power-on default - Feed(device, 27, (byte)'G', 0x00); + Feed(device, 27, (byte)'G', 0x00); // L4PLASMA.CPP's boot byte Assert.Equal(PlasmaCursorMode.Hidden, device.CursorMode); } @@ -302,6 +318,7 @@ public class VPlasmaDeviceTests Assert.Equal(0, device.Font); Assert.Equal(PlasmaAttributes.None, device.Attributes); Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); - Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); + Assert.Equal(0, device.CursorX); + Assert.Equal(0, device.CursorY); } }