Files
VRIO/tests/VPlasma.Core.Tests/VPlasmaDeviceTests.cs
T
CydandClaude Opus 4.8 95b701ae0a vPLASMA: make jumpers 4 (orientation) and 5 (display test) functional
Both now do what the firmware does, at the device level so the shared
PlasmaCanvas (and vRIO's embedded glass) are untouched:

- Jumper 4 → display orientation. VPlasmaDevice gains a PlasmaOrientation
  property; in Vertical the logical space is 32x128 and every dot is rotated
  onto the physical 128x32 glass via a new Plot() helper that all drawing
  (text, cursor, ESC P graphics, ESC Q/R bounds) now goes through. Jumper 4
  installed = horizontal (the normal cockpit setup, default), removed =
  vertical. Changing it re-inits the panel, like the boot strap read.
- Jumper 5 → ShowTestPattern(): lights every dot (the power-on dead-dot
  check the firmware runs at $B888). Removing the jumper clears it.

The config panel wires both up and shows the live orientation in the
counters. 27 unit tests pass (3 new: vertical rotation mapping, orientation
re-init, test pattern), including the unchanged horizontal path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:26:51 -05:00

374 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using VPlasma.Core.Device;
using Xunit;
namespace VPlasma.Core.Tests;
public class VPlasmaDeviceTests
{
private const byte Esc = 0x1B;
private static void Feed(VPlasmaDevice device, params byte[] bytes)
=> device.OnReceived(bytes, bytes.Length);
private static void Feed(VPlasmaDevice device, IEnumerable<byte> bytes)
{
byte[] arr = bytes.ToArray();
device.OnReceived(arr, arr.Length);
}
private static byte Pixel(VPlasmaDevice device, int x, int y)
{
var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height];
device.CopyFrame(frame);
return frame[y * VPlasmaDevice.Width + x];
}
/// <summary>A full-width ESC P row the way L4PLASMA.CPP sends one.</summary>
private static byte[] GraphicsRow(int y, params byte[] data)
{
var row = new List<byte> { Esc, (byte)'P', 0, (byte)y, 0, (byte)data.Length, 1 };
row.AddRange(data);
return row.ToArray();
}
// ---- graphics writes -------------------------------------------------
[Fact]
public void GraphicsRow_SetsPixelsMsbFirst()
{
var device = new VPlasmaDevice();
Feed(device, GraphicsRow(5, 0x80, 0x01)); // xbyte 0..1
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 5)); // MSB of byte 0
Assert.Equal(0, Pixel(device, 1, 5));
Assert.Equal(0, Pixel(device, 14, 5));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 15, 5)); // LSB of byte 1
Assert.Equal(0, Pixel(device, 0, 4));
Assert.Equal(0, Pixel(device, 0, 6));
Assert.Equal(1, device.GraphicsRows);
}
[Fact]
public void GraphicsRow_SurvivesAnyChunkBoundary()
{
byte[] wire = GraphicsRow(3, Enumerable.Repeat((byte)0xFF, 16).ToArray());
for (int split = 1; split < wire.Length; ++split)
{
var device = new VPlasmaDevice();
device.OnReceived(wire, split);
byte[] rest = wire.Skip(split).ToArray();
device.OnReceived(rest, rest.Length);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 3));
Assert.Equal(1, device.GraphicsRows);
}
}
[Fact]
public void GraphicsBlock_MultipleRowsAdvanceY()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'P', 0, 10, 0, 1, 3, 0x80, 0x80, 0x80);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 10));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 11));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 12));
Assert.Equal(0, Pixel(device, 0, 13));
Assert.Equal(3, device.GraphicsRows);
}
[Fact]
public void GraphicsWrite_HonorsByteColumnOffset()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'P', 0, 0, 2, 1, 1, 0xFF); // xbyte=2 → x 16..23
Assert.Equal(0, Pixel(device, 15, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 16, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 23, 0));
Assert.Equal(0, Pixel(device, 24, 0));
}
[Fact]
public void GraphicsWrite_OutOfRangeRowIsConsumedNotDrawn()
{
var device = new VPlasmaDevice();
Feed(device, GraphicsRow(40, Enumerable.Repeat((byte)0xFF, 16).ToArray()));
Feed(device, (byte)'H'); // parser must be back in text mode
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(0, device.GraphicsRows);
}
[Fact]
public void GraphicsWrite_OverwritesTextAttributes()
{
var device = new VPlasmaDevice();
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 (real font 0 = 6×8) -----------------------------------
[Fact]
public void Text_DrawsRealGlyphAndAdvancesByFontWidth()
{
var device = new VPlasmaDevice();
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, 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_MoveCursorByPixels()
{
var device = new VPlasmaDevice(); // font 0: 6 wide, 8 tall
Feed(device, 0x09, 0x09, 0x09); // HT ×3 → x = 18
Assert.Equal(18, device.CursorX);
Feed(device, 0x08); // BS → x = 12
Assert.Equal(12, device.CursorX);
Feed(device, 0x0A); // LF → y = 8
Assert.Equal(8, device.CursorY);
Assert.Equal(0, device.CursorX);
Feed(device, (byte)'H', 0x0D); // draw + CR → x back to 0
Assert.Equal(0, device.CursorX);
Feed(device, 0x0B); // VT → y back to 0
Assert.Equal(0, device.CursorY);
}
[Fact]
public void EscQ_EscR_SetPixelCursor()
{
var device = new VPlasmaDevice();
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, 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]
public void EscAt_ClearsScreenAndResetsTextState()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 4, Esc, (byte)'H', 1, (byte)'H');
Feed(device, Esc, (byte)'@');
Assert.Equal(0, Pixel(device, 0, 0));
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
Assert.Equal(0, device.Font);
Assert.Equal(PlasmaAttributes.None, device.Attributes);
}
[Fact]
public void EscL_HomesCursorWithoutClearing()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H', Esc, (byte)'L');
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); // glyph survives
}
[Theory]
[InlineData(0x00, PlasmaCursorMode.Hidden)] // the game's cursor-off
[InlineData(0xFF, PlasmaCursorMode.Hidden)] // the test tool's hide
[InlineData(0x01, PlasmaCursorMode.Steady)]
[InlineData(0x03, PlasmaCursorMode.Flashing)]
public void EscG_SetsCursorMode(byte operand, PlasmaCursorMode expected)
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'G', operand);
Assert.Equal(expected, device.CursorMode);
}
[Fact]
public void EscK_SelectsRealFonts()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 4); // large font: 12×16
Assert.Equal(4, device.Font);
Assert.Equal(12, device.FontWidth);
Assert.Equal(16, device.FontHeight);
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);
}
[Fact]
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)'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)'@');
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', 0); // defaults restored
Assert.Equal(PlasmaAttributes.None, device.Attributes);
}
// ---- robustness ----------------------------------------------------------
[Fact]
public void UnknownEscape_IsConsumedAndTextResumes()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'\\', (byte)'H'); // ESC '\' is not a command
Assert.Equal(1, device.TextCharsDrawn);
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
}
[Fact]
public void GameStartupSequence_HidesCursor()
{
var device = new VPlasmaDevice();
Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); // power-on default
Feed(device, 27, (byte)'G', 0x00); // L4PLASMA.CPP's boot byte
Assert.Equal(PlasmaCursorMode.Hidden, device.CursorMode);
}
[Fact]
public void SelfTestPages_ParseWithoutUnknownCommands()
{
for (int page = 0; page < PlasmaSelfTest.PageCount; ++page)
{
var device = new VPlasmaDevice();
var complaints = new List<string>();
device.Logged += line =>
{
if (line.StartsWith("Unknown", StringComparison.Ordinal)
|| line.StartsWith("Unhandled", StringComparison.Ordinal))
complaints.Add(line);
};
byte[] bytes = PlasmaSelfTest.BuildPage(page);
device.OnReceived(bytes, bytes.Length);
Assert.Empty(complaints);
Assert.True(device.BytesReceived > 0);
}
}
// ---- orientation + test pattern (JP1 jumpers 4 & 5) --------------------
[Fact]
public void Orientation_VerticalRotatesLogicalOntoPhysicalGlass()
{
var device = new VPlasmaDevice { Orientation = PlasmaOrientation.Vertical };
Assert.Equal(32, device.LogicalWidth);
Assert.Equal(128, device.LogicalHeight);
// Logical (0,0) maps to physical (px=ly=0, py=Height-1-lx=31).
Feed(device, Esc, (byte)'R', 0, Esc, (byte)'Q', 0, (byte)'H');
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 31)); // 'H' top-left dot
Assert.Equal(0, Pixel(device, 0, 0));
// ESC Q (row Y) now accepts up to the 128-tall logical height;
// ESC R (column X) is bounded by the 32-wide logical width.
Feed(device, Esc, (byte)'Q', 100);
Assert.Equal(100, device.CursorY);
Feed(device, Esc, (byte)'R', 40); // out of range for 32-wide vertical
Assert.NotEqual(40, device.CursorX);
}
[Fact]
public void Orientation_ChangeClearsAndHomes()
{
var device = new VPlasmaDevice();
Feed(device, (byte)'H');
Assert.NotEqual(0, Pixel(device, 0, 0));
device.Orientation = PlasmaOrientation.Vertical;
Assert.Equal(0, Pixel(device, 0, 0)); // cleared
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
}
[Fact]
public void ShowTestPattern_LightsEveryDot()
{
var device = new VPlasmaDevice();
device.ShowTestPattern();
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 31));
Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 64, 16));
device.Reset();
Assert.Equal(0, Pixel(device, 64, 16));
}
[Fact]
public void Reset_RestoresPowerOnState()
{
var device = new VPlasmaDevice();
Feed(device, Esc, (byte)'K', 5, Esc, (byte)'H', 4, Esc, (byte)'G', 0, (byte)'H');
device.Reset();
Assert.Equal(0, Pixel(device, 0, 0));
Assert.Equal(0, device.Font);
Assert.Equal(PlasmaAttributes.None, device.Attributes);
Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode);
Assert.Equal(0, device.CursorX);
Assert.Equal(0, device.CursorY);
}
}