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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user