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:
@@ -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** 0–9 (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, 0–127) |
|
||||
| `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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user