PlasmaNew: ESC X/x box commands in the replica + MW4 bench stream

Implements the firmware-disasm-confirmed Firestorm box protocol in the
MatrixPortal replica (ESC X outline / ESC x mode-fill via a BOXOP operand
collector, plus the ESC Y region op), updates FIRMWARE.md with the findings,
and adds FS-plasma-bench.ps1 -- a simulated MW4 serial stream for benching
the display without a pod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-24 12:02:37 -05:00
co-authored by Claude Fable 5
parent 110ecec2ca
commit bd082d8c4a
5 changed files with 387 additions and 14 deletions
+34 -13
View File
@@ -13,7 +13,10 @@ disassembler — the toolchain has no m68hc11 target).
- **CPU $8000$FFFF = EPROM upper 32 KB, 1:1** (the HC11 vector table lands at
ROM offset `$FFC0$FFFF` and is valid, which pins the mapping). The EPROM's
**lower 32 KB is unused** (all `$00`) — only A15-high is decoded to the ROM.
**lower 32 KB is CPU-invisible** (only A15-high is decoded to the ROM) and
unused — though not quite empty: a stray 1-bpp bitmap-shaped block sits at
file offsets `$0100$02FF` (233 non-zero bytes in 16-byte rows), dead data
left in the image by the EPROM's author.
- **Code:** `$9000$B8xx`. **Data/tables:** `$8000$8FFF` (demo), `$98AC+`
(dispatch tables), `$BC03+` (font descriptors), `$C000$DFFF` (glyph
bitmaps + graphics).
@@ -27,6 +30,7 @@ disassembler — the toolchain has no m68hc11 target).
|--------|--------|-------|
| RESET | `$9059` | Init: registers, stack `$0227`, then main loop |
| **SCI (serial rx)** | **`$B85C`** | Interrupt-driven receive → ring buffer |
| **PAOV (pulse-acc overflow)** | **`$9224`** | Display-scan ISR: reloads PACNT=`$E0`, flips the half-frame flag `$B0.0`, writes the scan base to the `$01F8` latch |
| COP watchdog | `$905E` | (re-inits) |
| others | `$9059` | default → reset |
@@ -49,13 +53,18 @@ disassembler — the toolchain has no m68hc11 target).
Two jump tables, indexed by byte:
- **`ESC` + letter → command table at `$98AC`.** Valid letters `0x30``0x7E`;
index = `letter 0x30`; null entry = ignored. **58 commands populated.**
index = `letter 0x30`; null entry = ignored. **57 commands populated**
(`q`/`r` share handler `$A5B7`).
- **Control bytes `0x08``0x14` → table at `$994C`.** index = `byte 0x08`.
Most command handlers share a prologue: first sighting of the letter sets the
"operand pending" flag and returns; the **next byte is the 1-byte operand**
(in `$C6`). Multi-operand commands (`ESC P/X/Y`) collect into a parameter
block at `$0070`.
(in `$C6`). Multi-operand commands (`ESC P/X/x/Y`) collect into a parameter
block at `$0070``P` via a counting collector, `X/x/Y` by re-entering
themselves through the `$C4` continuation with a sub-state bitmask at `$B5`,
validating each operand on arrival (an out-of-range operand aborts the whole
sequence with nothing drawn). Operand bytes are raw binary and are never
re-tokenized — a coordinate of 27 (`0x1B` = ESC) is fine.
### Control characters (`$994C`)
@@ -84,8 +93,9 @@ block at `$0070`.
| `ESC i n` | `$A4A2` | **Select DISPLAY page** 09 (page-flip; sets `$BC`) |
| `ESC P …` | `$AAF1` | **Graphics bitmap write** (multi-operand: screen,y,x,w,h,data) |
| `ESC A``ESC F` | `$9FB6``$A13C` | **Vector/graphics primitives** (line/point/move; pen state `$B6`, coords `$58/$59`, line routine `$A16C`) |
| `ESC X n` | `$A748` | **Set graphics pen X** (multi-op, 0127) |
| `ESC Y n` | `$A644` | **Set graphics pen Y** (multi-op) |
| `ESC X l t r b` | `$A748` | **Draw box outline** — 4 raw inclusive coords, validated on arrival (l<128, t<32, r≥l, b≥t). Solid top row; if taller than one row, solid bottom row + single-dot side columns. Pixels OR-set, both page halves. *This is Firestorm's score-panel box (`CPlasma::PlasmaBoxDraw`).* |
| `ESC x m l t r b` | `$A91A` | **Box region op** — mode ≤ 2 then the same 4 coords: **0 = blank, 1 = fill, 2 = invert** (fill worker `$AA07`). *Firestorm's `PlasmaBoxFill` always sends mode 0.* |
| `ESC Y m a b d` | `$A644` | **Screen logic-op blit** — mode ≤ 2, three screen numbers ≤ 9: `dest = srcA AND/OR/XOR srcB` over the whole 1 KB screen (address table `$AADD`, combiner `$AABC`) |
| `ESC J` | `$A4D4` | **Toggle** mode bit `$B7.7` (orientation/display — TBD) |
### ESC commands — populated but not yet decoded
@@ -93,7 +103,8 @@ block at `$0070`.
`ESC 0``9` (`$9E27`+, set continuations — likely custom-char / numeric entry),
`ESC : ; =` , `ESC < > W w _` (cluster `$AEBA$AF00`), `ESC B C D E F` variants,
`ESC M N O` (`$A5BD/$A5C5/$A5E0`), `ESC Z ^ z ~` (cluster `$AC73$ACA1`),
`ESC a``f`, `ESC h l n p q r x`. ~30 handlers remain to label — full list with
`ESC a``f` (the erase-mode twins of `A``F`: same bodies with pen bit `$B6.7`
set), `ESC h l n p q r`. ~29 handlers remain to label — full list with
addresses is dumped by the tooling below.
## Fonts
@@ -113,10 +124,9 @@ FONTS"):
| 6 | `0x20``0xFF` | 7×10 | |
| 7 | `0x40``0x7F` | 7×10 | |
Glyph bitmaps live in ROM (`~$C000$DFFF`). **Exact glyph base + encoding
pending** — the renderer at `$9648` enqueues to a deferred rasterizer; tracing
that (or brute-forcing the 'A' pattern at the known stride) will extract the
real glyphs to replace vPLASMA's public-domain 5×7 stand-in.
Glyph bitmaps live in ROM (`~$C000$DFFF`) and have been **extracted** — all
8 fonts are generated into vPLASMA's `PlasmaFonts.cs` and the replica's
`plasma_fonts.h` (row = `uint16`, bit 15 = leftmost pixel).
## Demo program
@@ -147,8 +157,19 @@ double-buffered pages (`ESC I`/`ESC i`) and the vector-graphics primitives
**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
page-flip, 8 fonts, text attributes as 4 flags, plus vector graphics. All of
it ports directly onto a modern MCU. The one artifact still to extract is the
glyph bitmaps.
it ports directly onto a modern MCU.
**2026-07-22 — the box family decoded (Firestorm unblocked):** `ESC X`/`ESC x`
turned out to be box-outline / box-region commands (the earlier "graphics pen
X/Y" reading was wrong — corrected in the table above), and they are exactly
what **BattleTech: Firestorm**'s `CPlasma` driver (`mw4\Code\MW4\CRIOMAIN.CPP`)
uses for its rank/score panel, alongside cursor-addressed text. BT/RP 4.10 by
contrast drive the panel purely with `ESC P` full-line redraws (`L4PLASMA.CPP`).
Both `ESC X` and `ESC x` (plus `ESC Y` as consume-and-validate, no-op in the
single-page model) are now implemented in the
[hardware replica](replica/MatrixPortalPlasma/PlasmaDisplay.cpp); vPLASMA still
needs the same additions to stay the oracle. `FS-plasma-bench.ps1` replays the
Firestorm sequence for testing.
## Reproduce
+247
View File
@@ -0,0 +1,247 @@
<#
plasma-bench.ps1 -- Bench-test driver for the MW4/FireStorm cockpit plasma score display.
Replays the exact byte sequences MW4.exe sends to the plasma display on COM2
(class CPlasma over C232Comm; sources: mw4\Code\MW4\CRIOMAIN.CPP + c232comm.cpp).
Port settings (c232comm.cpp Open()): 9600 baud, 8 data bits, no parity, 1 stop bit,
no CTS/RTS/XON flow control; DTR is pulsed high at open then dropped ~10 ms later
(DTR_CONTROL_ENABLE -> DTR_CONTROL_DISABLE + Sleep(10)). Bytes go out raw, unframed.
Command set (ESC = 0x1B), CRIOMAIN.CPP lines 2998-3139:
ESC @ clear screen (PlasmaClear)
ESC G n cursor mode (PlasmaCursor: 0=off 1=on 2=flash)
ESC R n cursor X (PlasmaCursorX)
ESC Q n cursor Y (PlasmaCursorY)
ESC H n font attribute (PlasmaFontAttr: 0=normal 1=half 2=line 4=reverse 8=flash)
ESC K n font select (PlasmaFont: 0-3,6,7 = 5x7 4,5 = 10x14)
ESC X l t r b draw box outline (PlasmaBoxDraw)
ESC x 0 l t r b fill/blank box (PlasmaBoxFill)
<ascii bytes> text at current cursor (PlasmaText)
Game sequence replayed here (PlasmaDisplay, CRIOMAIN.CPP:3281):
startup clear + cursor off (CPlasma ctor)
state 1 ready centered 9-space wipe, then centered callsign
(<=9 chars -> font 5 [10x14]; 10-20 chars -> font 2 [5x7])
state 2 start score panel: fill(27,19,93,30), boxes (27,19,42,30)+(42,19,93,30);
rank in left box (x=33 one digit / x=29 two), score right-aligned
ending at x=85-5*len; both font 2, y=20. Later updates redraw only
the field that changed (no boxes) -- use -CountUp to exercise that.
state 0 end clear (-ClearAtEnd)
Examples:
.\plasma-bench.ps1 -Port COM5 -Callsign JEFF -Rank 3 -Score 4200
.\plasma-bench.ps1 -Port COM5 -Callsign JEFF -CountUp 5 -CountUpInterval 1.5
.\plasma-bench.ps1 -DryRun # print the byte stream, touch no port
#>
param(
[string]$Port = 'COM2',
[int]$Baud = 9600,
[string]$Callsign = 'FStest6789012',
[ValidateRange(1, 99)][int]$Rank = 1,
[ValidateRange(0, 99999999)][int]$Score = 1250,
[double]$DelaySeconds = 5, # callsign hold time before the score panel appears
[int]$CountUp = 0, # extra score updates after the first draw (simulates in-game scoring)
[double]$CountUpInterval = 2, # seconds between count-up updates
[int]$CountUpStep = 150, # score increment per update
[switch]$ClearAtEnd, # cursor-off + clear before closing (game does this at mission end)
[switch]$DtrPulse, # replicate the game's DTR pulse at open (some USB CDC devices choke on it)
[switch]$HexDump, # print every packet as hex as it is sent
[switch]$DryRun # no serial port; just print the packets (implies -HexDump)
)
$ErrorActionPreference = 'Stop'
$ESC = [byte]0x1B
if ($DryRun) { $HexDump = $true }
# --- serial plumbing (C232Comm) ---------------------------------------------
$script:SerialPort = $null
function New-PlasmaPort {
$sp = New-Object System.IO.Ports.SerialPort $Port, $Baud,
([System.IO.Ports.Parity]::None), 8, ([System.IO.Ports.StopBits]::One)
$sp.Handshake = [System.IO.Ports.Handshake]::None
$sp.WriteTimeout = 1000
return $sp
}
function Open-Plasma {
if ($DryRun) { return }
# DEFAULT: plain open, modem lines untouched. The plasma protocol is data bytes
# only; the game's DTR pulse (DTR_CONTROL_ENABLE at open -> DISABLE after
# Sleep(10), c232comm.cpp) is generic port-open code, not display protocol, and
# a failed modem-line request on a USB CDC device (usbser.sys) faults the .NET
# serial stream and can knock the device off the bus. -DtrPulse opts back in
# for a real RS-232 wiring that expects it.
$sp = New-PlasmaPort
if ($DtrPulse) {
try {
$sp.RtsEnable = $false # fRtsControl = 0
$sp.DtrEnable = $true # DTR_CONTROL_ENABLE at open...
$sp.Open()
Start-Sleep -Milliseconds 10
$sp.DtrEnable = $false # ...then DTR_CONTROL_DISABLE after Sleep(10)
} catch {
Write-Warning "DTR pulse failed ($($_.Exception.Message.Trim())); reopening without touching modem lines."
try { $sp.Dispose() } catch { }
# wait for the device to settle / re-enumerate if the failed request upset it
$deadline = (Get-Date).AddSeconds(10)
while (((Get-Date) -lt $deadline) -and
([System.IO.Ports.SerialPort]::GetPortNames() -notcontains $Port)) {
Start-Sleep -Milliseconds 250
}
Start-Sleep -Milliseconds 500
$sp = New-PlasmaPort
$sp.Open()
}
} else {
$sp.Open() # .NET defaults: DTR/RTS not asserted
}
Start-Sleep -Milliseconds 200 # let USB CDC settle before the first write
$script:SerialPort = $sp
}
function Send-Packet {
# CPlasma::SendPacket -> txComLoop: one raw write per command, no framing
param([byte[]]$Bytes)
if ($HexDump) {
$hex = ($Bytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' '
$txt = -join ($Bytes | ForEach-Object { if ($_ -ge 0x20 -and $_ -le 0x7E) { [char]$_ } else { '.' } })
Write-Host (' TX {0,-42} {1}' -f $hex, $txt)
}
if ($script:SerialPort) { $script:SerialPort.Write($Bytes, 0, $Bytes.Length) }
}
# --- CPlasma command primitives (CRIOMAIN.CPP:2998-3097) --------------------
function Plasma-Clear { Send-Packet ([byte[]]($ESC, 0x40)) } # ESC @
function Plasma-Cursor { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x47, $n)) } # ESC G
function Plasma-CursorX { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x52, $n)) } # ESC R
function Plasma-CursorY { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x51, $n)) } # ESC Q
function Plasma-FontAttr { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x48, $n)) } # ESC H
function Plasma-Font { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x4B, $n)) } # ESC K
function Plasma-BoxDraw {
param([int]$l, [int]$t, [int]$r, [int]$b)
Send-Packet ([byte[]]($ESC, 0x58, $l, $t, $r, $b)) # ESC X
}
function Plasma-BoxFill {
param([int]$l, [int]$t, [int]$r, [int]$b)
Send-Packet ([byte[]]($ESC, 0x78, 0, $l, $t, $r, $b)) # ESC x 0
}
function Get-FontSize {
# CPlasma::GetFontSize: fonts 0-3,6,7 = 5x7; fonts 4,5 = 10x14
param([int]$Font)
if ($Font -eq 4 -or $Font -eq 5) { return @{ W = 10; H = 14 } }
return @{ W = 5; H = 7 }
}
function Plasma-PosText {
# CPlasma::PlasmaPosText, including its auto font selection + centering math
param([string]$Text, [int]$X = 0, [int]$Y = 0, [int]$Attr = 0, [int]$Font = 5)
if (-not $Text) { return }
$len = $Text.Length
if ($len -le 0) { return }
if ($Font -ne 2) { # auto-pick: big font up to 9 chars, else small, cap 20
if ($len -le 9) { $Font = 5 }
elseif ($len -le 20) { $Font = 2 }
else { $Font = 2; $len = 20 }
} elseif ($len -gt 20) { $len = 20 }
$size = Get-FontSize $Font
if ($X -eq 0 -and $Y -eq 0) { # center on the 112-wide panel (C++ integer division)
$X = 56 - [math]::Floor(($len * $size.W) / 2)
$Y = 15 - [math]::Floor($size.H / 2)
}
Plasma-CursorX $X
Plasma-CursorY $Y
Plasma-FontAttr $Attr
Plasma-Font $Font
Send-Packet ([System.Text.Encoding]::ASCII.GetBytes($Text.Substring(0, $len)))
}
# --- game-level draws (CRIOMAIN.CPP:3141-3308) -------------------------------
$script:OldRank = $null # m_nOldRank / m_nOldScore (INT_MIN after "ready")
$script:OldScore = $null
function Plasma-ScoreDraw {
# CPlasma::PlasmaScoreDraw: boxes on first draw, then changed-fields-only updates
param([string]$RankStr, [string]$ScoreStr, [bool]$BoxFlag)
if ($BoxFlag) {
Plasma-BoxFill 27 19 93 30
Plasma-BoxDraw 27 19 42 30
Plasma-BoxDraw 42 19 93 30
}
if ($RankStr.Length -ge 1 -and $RankStr.Length -le 2 -and
$ScoreStr.Length -ge 1 -and $ScoreStr.Length -le 8) {
if ($script:OldRank -ne [int]$RankStr) {
Plasma-PosText ' ' 29 20 0 2
if ($RankStr.Length -eq 1) { Plasma-PosText $RankStr 33 20 0 2 }
else { Plasma-PosText $RankStr 29 20 0 2 }
}
if ($script:OldScore -ne [int]$ScoreStr) {
Plasma-PosText ' ' 44 20 0 2
Plasma-PosText $ScoreStr (85 - ($ScoreStr.Length * 5)) 20 0 2
}
} else {
Write-Warning "Rank must be 1-2 chars and score 1-8 chars; skipping draw (game does the same)."
}
$script:OldRank = [int]$RankStr
$script:OldScore = [int]$ScoreStr
}
# --- main: replay the game's display sequence --------------------------------
if ($Callsign.Length -gt 20) { $Callsign = $Callsign.Substring(0, 20) }
try {
if (-not $DryRun) { Write-Host "Opening $Port @ $Baud 8N1 (no flow control)..." }
Open-Plasma
# CPlasma ctor: clear + cursor off
Write-Host '[startup] clear + cursor off'
Plasma-Clear
Plasma-Cursor 0
# state 1 "ready": 9-space wipe then the callsign, both auto-centered
Write-Host "[ready] callsign '$Callsign'"
Plasma-PosText ' '
Plasma-PosText $Callsign
Write-Host ("[wait] {0}s" -f $DelaySeconds)
if (-not $DryRun) { Start-Sleep -Seconds $DelaySeconds }
# state 2 "start": first score draw includes the panel boxes
Write-Host "[score] rank $Rank, score $Score (panel + fields)"
Plasma-ScoreDraw "$Rank" "$Score" $true
for ($i = 1; $i -le $CountUp; $i++) {
if (-not $DryRun) { Start-Sleep -Seconds $CountUpInterval }
$Score += $CountUpStep
Write-Host "[score] update ${i}: score $Score (changed fields only)"
Plasma-ScoreDraw "$Rank" "$Score" $false
}
if ($ClearAtEnd) {
# CPlasma dtor: cursor off + clear
Write-Host '[end] cursor off + clear'
Plasma-Cursor 0
Plasma-Clear
}
Write-Host 'Done.'
}
finally {
if ($script:SerialPort) {
$script:SerialPort.Close()
$script:SerialPort.Dispose()
}
}
@@ -7,6 +7,7 @@ static const uint8_t CMD_CLEAR = '@', CMD_CURSOR = 'G', CMD_ATTR = 'H';
static const uint8_t CMD_FONT = 'K', CMD_HOME = 'L', CMD_GRAPHICS = 'P';
static const uint8_t CMD_SETROW = 'Q', CMD_SETCOL = 'R';
static const uint8_t CMD_DRAWPAGE = 'I', CMD_DISPPAGE = 'i';
static const uint8_t CMD_BOXDRAW = 'X', CMD_BOXFILL = 'x', CMD_SCREENOP = 'Y';
PlasmaDisplay::PlasmaDisplay() { reset(); }
@@ -21,6 +22,7 @@ void PlasmaDisplay::reset() {
state_ = TEXT;
headerFill_ = 0;
dataIndex_ = dataLength_ = 0;
boxFill_ = 0;
dirty_ = true;
}
@@ -68,6 +70,19 @@ void PlasmaDisplay::plot(int lx, int ly, uint8_t flags) {
pixels_[py * WIDTH + px] = flags;
}
uint8_t PlasmaDisplay::peek(int lx, int ly) const {
if ((unsigned)lx >= (unsigned)logicalW() || (unsigned)ly >= (unsigned)logicalH()) return 0;
int px, py;
if (orient_ == HORIZONTAL) {
px = lx;
py = ly;
} else {
px = ly;
py = HEIGHT - 1 - lx;
}
return pixels_[py * WIDTH + px];
}
void PlasmaDisplay::feed(const uint8_t *buf, size_t n) {
for (size_t i = 0; i < n; ++i) feed(buf[i]);
}
@@ -85,6 +100,7 @@ void PlasmaDisplay::feed(uint8_t b) {
if (headerFill_ == 5) beginGraphics();
break;
case GDATA: stepGraphics(b); break;
case BOXOP: stepBoxOperand(b); break;
}
}
@@ -134,6 +150,13 @@ void PlasmaDisplay::stepEscape(uint8_t b) {
headerFill_ = 0;
state_ = GHEADER;
break;
case CMD_BOXDRAW:
case CMD_BOXFILL:
case CMD_SCREENOP:
boxCmd_ = b;
boxFill_ = 0;
state_ = BOXOP;
break;
default: break; // unknown command: ignored (does not consume an operand)
}
}
@@ -170,6 +193,73 @@ void PlasmaDisplay::applyOperand(uint8_t cmd, uint8_t op) {
}
}
// ESC X / ESC x / ESC Y collect operands one byte at a time, each validated on
// arrival; an out-of-range operand aborts the whole sequence with nothing drawn
// (firmware $A748/$A91A/$A644: BCLR $AF #$10). Operand bytes are never
// re-tokenized, so coordinate values that collide with control bytes are fine —
// Firestorm's real panel left edge is 27, which is ESC on the wire.
void PlasmaDisplay::stepBoxOperand(uint8_t b) {
bool ok;
if (boxCmd_ == CMD_SCREENOP) {
// ESC Y mode,srcA,srcB,dest — whole-screen AND/OR/XOR between page
// buffers. Validated and consumed but a no-op here: single-page, like
// ESC I / ESC i.
ok = (boxFill_ == 0) ? (b <= 2) : (b <= 9);
if (!ok) { state_ = TEXT; return; }
box_[boxFill_++] = b;
if (boxFill_ == 4) state_ = TEXT;
return;
}
// ESC x leads with a mode byte; the coordinates then follow in both commands
// as left, top, right, bottom — inclusive, right >= left, bottom >= top.
int coordIdx = (boxCmd_ == CMD_BOXFILL) ? boxFill_ - 1 : boxFill_;
switch (coordIdx) {
case -1: ok = b <= 2; break; // mode: 0 blank, 1 fill, 2 invert
case 0: ok = b < logicalW(); break;
case 1: ok = b < logicalH(); break;
case 2: ok = b < logicalW() && b >= box_[boxFill_ - 2]; break;
default: ok = b < logicalH() && b >= box_[boxFill_ - 2]; break;
}
if (!ok) { state_ = TEXT; return; }
box_[boxFill_++] = b;
int need = (boxCmd_ == CMD_BOXFILL) ? 5 : 4;
if (boxFill_ < need) return;
state_ = TEXT;
if (boxCmd_ == CMD_BOXDRAW) drawBoxOutline(box_[0], box_[1], box_[2], box_[3]);
else applyBoxRegion(box_[0], box_[1], box_[2], box_[3], box_[4]);
}
// ESC X: rectangle outline ($A748) — solid top row; if taller than one row,
// solid bottom row with single-dot side columns between. Dots are OR-set, so
// existing content survives (and a half-intensity dot goes full, as the
// firmware sets the bit in both page halves).
void PlasmaDisplay::drawBoxOutline(int l, int t, int r, int b) {
for (int x = l; x <= r; ++x) plot(x, t, PIX_LIT);
if (b > t) {
for (int x = l; x <= r; ++x) plot(x, b, PIX_LIT);
for (int y = t + 1; y < b; ++y) {
plot(l, y, PIX_LIT);
plot(r, y, PIX_LIT);
}
}
dirty_ = true;
}
// ESC x: rectangular region op ($A91A, fill worker $AA07) — mode 0 blanks,
// 1 fills solid, 2 inverts. Firestorm always sends mode 0 to wipe the score
// panel before boxing it.
void PlasmaDisplay::applyBoxRegion(int mode, int l, int t, int r, int b) {
for (int y = t; y <= b; ++y)
for (int x = l; x <= r; ++x) {
switch (mode) {
case 0: plot(x, y, 0); break;
case 1: plot(x, y, PIX_LIT); break;
default: plot(x, y, (peek(x, y) & PIX_LIT) ? 0 : PIX_LIT); break;
}
}
dirty_ = true;
}
void PlasmaDisplay::beginGraphics() {
int w = header_[3], h = header_[4];
dataLength_ = w * h;
@@ -60,12 +60,15 @@ class PlasmaDisplay {
Orientation orient_;
// Parser state.
enum State { TEXT, ESCAPE, OPERAND, GHEADER, GDATA };
enum State { TEXT, ESCAPE, OPERAND, GHEADER, GDATA, BOXOP };
State state_;
uint8_t pendingCmd_;
uint8_t header_[5]; // screen, y, x, w, h
int headerFill_;
int dataIndex_, dataLength_;
uint8_t boxCmd_; // ESC X / x / Y multi-operand collector
uint8_t box_[5]; // X: l,t,r,b x: mode,l,t,r,b Y: mode,srcA,srcB,dest
int boxFill_;
bool dirty_;
@@ -73,9 +76,13 @@ class PlasmaDisplay {
int logicalH() const { return orient_ == HORIZONTAL ? HEIGHT : WIDTH; }
void plot(int lx, int ly, uint8_t flags);
uint8_t peek(int lx, int ly) const;
void stepText(uint8_t b);
void stepEscape(uint8_t b);
void applyOperand(uint8_t cmd, uint8_t op);
void stepBoxOperand(uint8_t b);
void drawBoxOutline(int l, int t, int r, int b);
void applyBoxRegion(int mode, int l, int t, int r, int b);
void beginGraphics();
void stepGraphics(uint8_t b);
void drawChar(uint8_t code);
+8
View File
@@ -147,3 +147,11 @@ VPlasma.Wire decode --in logo.txt --png logo.png
Still deferred (as in vPLASMA, documented in `../FIRMWARE.md`): the 10
double-buffered pages (`ESC I`/`ESC i` are consumed but single-page) and the
vector-graphics primitives (`ESC A``F`).
**Ahead of vPLASMA (2026-07-22):** the box family is implemented here from the
firmware disassembly — `ESC X l t r b` (box outline) and `ESC x m l t r b`
(region blank/fill/invert), the commands **BattleTech: Firestorm**'s `CPlasma`
driver draws its score panel with; `ESC Y` (screen blit) is validated and
consumed as a no-op. vPLASMA needs the same additions before the differential
test covers Firestorm streams — until then `../FS-plasma-bench.ps1` is the
Firestorm exerciser.