Files
TeslaRel410/restoration/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.cpp
T
CydandClaude Fable 5 bd082d8c4a 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>
2026-07-24 12:02:37 -05:00

316 lines
10 KiB
C++

#include "PlasmaDisplay.h"
// Command bytes (see PlasmaProtocol.cs / FIRMWARE.md).
static const uint8_t ESC = 0x1B;
static const uint8_t BS = 0x08, HT = 0x09, LF = 0x0A, VT = 0x0B, CR = 0x0D;
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(); }
void PlasmaDisplay::reset() {
memset(pixels_, 0, sizeof(pixels_));
cx_ = cy_ = 0;
font_ = 0;
face_ = &plasmaFonts[0];
attrs_ = 0;
cursorMode_ = CURSOR_STEADY; // power-on default; the game hides it
orient_ = HORIZONTAL;
state_ = TEXT;
headerFill_ = 0;
dataIndex_ = dataLength_ = 0;
boxFill_ = 0;
dirty_ = true;
}
void PlasmaDisplay::setOrientation(Orientation o) {
if (orient_ == o) return;
orient_ = o;
memset(pixels_, 0, sizeof(pixels_));
cx_ = cy_ = 0;
dirty_ = true;
}
// The real display's power-on test (JP1 jumper 5, firmware $B888) runs through
// several drawing sequences — solid, border+grid, and more — to expose dead
// dots and addressing faults. These stand in for that; the sketch cycles them.
static bool testDot(int index, int x, int y) {
switch (index) {
case 0: return true; // all on
case 1: return x == 0 || x == PlasmaDisplay::WIDTH - 1 || // frame
y == 0 || y == PlasmaDisplay::HEIGHT - 1 ||
(x % 16) == 0 || (y % 8) == 0; // + grid
case 2: return ((y / 2) % 2) == 0; // horizontal stripes
case 3: return ((x / 4) % 2) == 0; // vertical stripes
default: return (((x / 8) + (y / 8)) % 2) == 0; // checkerboard
}
}
void PlasmaDisplay::showTestPattern(int index) {
for (int y = 0; y < HEIGHT; ++y)
for (int x = 0; x < WIDTH; ++x)
pixels_[y * WIDTH + x] = testDot(index, x, y) ? PIX_LIT : 0;
dirty_ = true;
}
// One logical dot → the physical 128x32 buffer, rotated per orientation.
void PlasmaDisplay::plot(int lx, int ly, uint8_t flags) {
if ((unsigned)lx >= (unsigned)logicalW() || (unsigned)ly >= (unsigned)logicalH()) return;
int px, py;
if (orient_ == HORIZONTAL) {
px = lx;
py = ly;
} else {
px = ly;
py = HEIGHT - 1 - lx; // 90° rotation onto landscape glass
}
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]);
}
void PlasmaDisplay::feed(uint8_t b) {
switch (state_) {
case TEXT: stepText(b); break;
case ESCAPE: stepEscape(b); break;
case OPERAND:
state_ = TEXT;
applyOperand(pendingCmd_, b);
break;
case GHEADER:
header_[headerFill_++] = b;
if (headerFill_ == 5) beginGraphics();
break;
case GDATA: stepGraphics(b); break;
case BOXOP: stepBoxOperand(b); break;
}
}
void PlasmaDisplay::stepText(uint8_t b) {
switch (b) {
case ESC: state_ = ESCAPE; return;
case BS: cx_ = max(0, cx_ - face_->width); dirty_ = true; return;
case HT: advanceCursor(); dirty_ = true; return;
case LF: nextLine(); dirty_ = true; return;
case VT:
cy_ -= face_->height;
if (cy_ < 0) cy_ = max(0, logicalH() - face_->height);
dirty_ = true;
return;
case CR: cx_ = 0; dirty_ = true; return;
}
if (b < 0x20) return; // any other control byte: swallow (matches firmware)
drawChar(b);
}
void PlasmaDisplay::stepEscape(uint8_t b) {
state_ = TEXT;
switch (b) {
case CMD_CLEAR:
memset(pixels_, 0, sizeof(pixels_));
cx_ = cy_ = 0;
font_ = 0;
face_ = &plasmaFonts[0];
attrs_ = 0;
dirty_ = true;
break;
case CMD_HOME:
cx_ = cy_ = 0;
dirty_ = true;
break;
case CMD_CURSOR:
case CMD_FONT:
case CMD_ATTR:
case CMD_SETROW:
case CMD_SETCOL:
case CMD_DRAWPAGE:
case CMD_DISPPAGE:
pendingCmd_ = b;
state_ = OPERAND;
break;
case CMD_GRAPHICS:
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)
}
}
void PlasmaDisplay::applyOperand(uint8_t cmd, uint8_t op) {
switch (cmd) {
case CMD_CURSOR:
cursorMode_ = (op == 0x00 || op == 0xFF) ? CURSOR_HIDDEN
: (op & 0x02) ? CURSOR_FLASHING
: CURSOR_STEADY;
dirty_ = true;
break;
case CMD_FONT:
if (op < 8) { // 8 real fonts; firmware ignores larger operands
font_ = op;
face_ = &plasmaFonts[op];
if (cx_ > logicalW() - 1) cx_ = logicalW() - 1;
if (cy_ > logicalH() - 1) cy_ = logicalH() - 1;
dirty_ = true;
}
break;
case CMD_ATTR:
attrs_ = op & 0x0F; // low 4 bits: half/underline/reverse/flash
break;
case CMD_SETROW:
if (op < logicalH()) { cy_ = op; dirty_ = true; }
break;
case CMD_SETCOL:
if (op < logicalW()) { cx_ = op; dirty_ = true; }
break;
case CMD_DRAWPAGE:
case CMD_DISPPAGE:
break; // page select: consumed but single-page (see FIRMWARE.md)
}
}
// 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;
dataIndex_ = 0;
state_ = dataLength_ > 0 ? GDATA : TEXT;
}
void PlasmaDisplay::stepGraphics(uint8_t b) {
int w = header_[3];
int rowOfBlock = dataIndex_ / w;
int byteOfRow = dataIndex_ % w;
int y = header_[1] + rowOfBlock;
int baseX = (header_[2] + byteOfRow) * 8;
// MSB = leftmost pixel (L4PLASMA.CPP packs 0x80 first).
for (int bit = 0; bit < 8; ++bit)
plot(baseX + bit, y, (b & (0x80 >> bit)) ? PIX_LIT : 0);
dirty_ = true;
if (++dataIndex_ >= dataLength_) state_ = TEXT;
}
void PlasmaDisplay::drawChar(uint8_t code) {
if (!faceHas(code)) return; // firmware ignores out-of-range chars
int w = face_->width, h = face_->height;
bool reverse = attrs_ & 0x04;
bool underline = attrs_ & 0x02;
uint8_t litFlags = PIX_LIT;
if (attrs_ & 0x01) litFlags |= PIX_HALF;
if (attrs_ & 0x08) litFlags |= PIX_FLASH;
for (int row = 0; row < h; ++row) {
uint16_t bits = faceRow(code, row); // bit15 = leftmost pixel
for (int col = 0; col < w; ++col) {
bool on = bits & (0x8000 >> col);
if (underline && row == h - 1) on = true;
if (reverse) on = !on;
plot(cx_ + col, cy_ + row, on ? litFlags : 0);
}
}
dirty_ = true;
advanceCursor();
}
void PlasmaDisplay::advanceCursor() {
cx_ += face_->width;
if (cx_ > logicalW() - face_->width) nextLine();
}
void PlasmaDisplay::nextLine() {
cx_ = 0;
cy_ += face_->height;
if (cy_ > logicalH() - face_->height) cy_ = 0; // wrap to top; no scroll
}