diff --git a/restoration/PlasmaNew/FIRMWARE.md b/restoration/PlasmaNew/FIRMWARE.md index 5959af57..e60c825f 100644 --- a/restoration/PlasmaNew/FIRMWARE.md +++ b/restoration/PlasmaNew/FIRMWARE.md @@ -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 diff --git a/restoration/PlasmaNew/FS-plasma-bench.ps1 b/restoration/PlasmaNew/FS-plasma-bench.ps1 new file mode 100644 index 00000000..7ddfd05a --- /dev/null +++ b/restoration/PlasmaNew/FS-plasma-bench.ps1 @@ -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) + 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() + } +} diff --git a/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.cpp b/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.cpp index e2c0a316..5424f481 100644 --- a/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.cpp +++ b/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.cpp @@ -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; diff --git a/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.h b/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.h index 7b96d39d..65e5f8a8 100644 --- a/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.h +++ b/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.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); diff --git a/restoration/PlasmaNew/replica/README.md b/restoration/PlasmaNew/replica/README.md index 0e7defba..87ba793f 100644 --- a/restoration/PlasmaNew/replica/README.md +++ b/restoration/PlasmaNew/replica/README.md @@ -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.