mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-31 12:05:05 +02:00
413 lines
17 KiB
Markdown
413 lines
17 KiB
Markdown
# First contact with the real VT1200
|
||
|
||
Date: 2026-10-18. Terminal at **100.103.100.103** (static IP, no BOOTP),
|
||
reached from host `100.103.255.254` on `enp197s0f3u1`.
|
||
|
||
**Read-only throughout this note — no `poke`, no `jump`.**
|
||
|
||
## Results
|
||
|
||
| check | result |
|
||
|---|---|
|
||
| TCP 771 | **open** |
|
||
| opcode 7 | challenge (flag `0x7F`) sent, computed reply accepted, `Done\n\0` |
|
||
| `peek 0x3F200000` (8 B) | `e9 7a e2 f5 aa aa 55 55` — **matches `vt1200_v22.bin`** |
|
||
| `peek 0xAECE0` (1 B) | `bd` — message classes 0,1,2 admitted (emulator: `05`) |
|
||
| `peek 0x1E8BA0` (4 B) | `0x018BF1B0` bits = **3,251,638 bytes** free in the heap |
|
||
| screenshot over `peek` | `dwt/evidence/real-unit-canvas.png` |
|
||
|
||
The screenshot shows the live Terminal Manager V2.2: Ethernet
|
||
`08-00-2B-16-4B-5A`, IP `100.103.100.103`, LAT `LAT_08002B164B5A`, and five
|
||
`Debug connection from IP host 100.103.255.254` lines in Messages — one per
|
||
connection made here. That is the read primitive working on real hardware.
|
||
|
||
## Two differences from the emulator, worth knowing
|
||
|
||
1. **The real unit challenges opcode 7.** The emulator only challenges opcodes
|
||
≥ 2. A plain liveness check therefore exercises the challenge too, and its
|
||
success is additional evidence that `dwt/challenge.py` matches the shipped
|
||
image.
|
||
2. **Message filter is `0xBD`, not `0x05`.** Levels 0, 1 and 2 are admitted on
|
||
the real unit; the emulator only admitted 0 and 3.
|
||
|
||
## Geometry
|
||
|
||
The raw VRAM canvas is **1024×1024**, captured correctly. The visible screen is
|
||
1280×1024, with the canvas at x=192; `dwt/screenshot.py --screen` uses the
|
||
emulator's 1280×901/offset-192×36 defaults and will misplace the bottom on the
|
||
real unit. The raw canvas is the authoritative artifact; for a faithful screen
|
||
image use `--screen-width 1280 --screen-height 1024 --offset-x 192 --offset-y 0`
|
||
once the vertical origin is measured.
|
||
|
||
## Next
|
||
|
||
The remaining unproven step on this unit is the write path: `poke`/`jump` and
|
||
then the COFF loader + `create_process`. The loader already refuses a target
|
||
outside a free-list block (status 8), so a first attempt with `marker.coff`
|
||
will tell us whether the linked ranges are free before anything runs.
|
||
|
||
## Display geometry and background (measured)
|
||
|
||
* Framebuffer **1024×1024, 128 bytes/row**, LSB-first. A 160-byte pitch shears;
|
||
128 bytes is coherent. The 160-byte figure is the **total raster**
|
||
(1280/8), not the memory pitch (`RENDER_REPORT.md`).
|
||
* Visible raster **1280×1024** (monitor OSD: 77.2 kHz / 73 Hz; Samsung
|
||
SyncMaster 770 native 1280×1024). The 1024-wide canvas is displayed 5:4, so
|
||
`dwt/screenshot.py --stretch 1280x1024` matches the screen. `--screen`'s
|
||
emulator 1280×901 geometry pads/crops and looks line-doubled.
|
||
* The `101010` background is **real data**, not a decode error. It is a 50%
|
||
stipple: rows alternate `55` (`01010101`) and `ff`. The root background has a
|
||
setting; setting it to **100% white** changed the root to solid `ff` (verified
|
||
by a fresh grab). The Terminal Manager window kept its own stipple, so its
|
||
window background is a **separate setting** (or was taken at creation).
|
||
* The bottom message box `Ethernet Port Error - 24` is present in VRAM; it is a
|
||
real terminal error, not a capture artifact.
|
||
|
||
### The TM stipple survives the root-background change
|
||
|
||
A fresh grab after the root was set to 100% white confirms: root `0xff`,
|
||
TM body still alternating `0x55`/`0xff` rows. So the TM pattern is not the
|
||
desktop background setting and the manager has no option for it.
|
||
|
||
The likely explanation is the **display fetch**, not the data: if the video
|
||
mode shows only one set of rows (line doubling / interlace), the `0xff` rows are
|
||
what reaches the glass and the `0x55` rows are never shown — which is why the TM
|
||
looks white while the raw framebuffer looks striped. That also explains why the
|
||
capture looked "line doubled": it is showing both row sets.
|
||
|
||
The emulator forces the **VR150** monitor position (`IP4-IP6` high, `0x70`).
|
||
The real unit's rear switch selects the mode; if it is at another position the
|
||
firmware may use a different fetch/pattern. Test: set the switch to position 1
|
||
(VR150), power-cycle, grab again.
|
||
|
||
### Line-interleaved framebuffer — the "line doubled" answer
|
||
|
||
The real framebuffer is **line-interleaved**: even rows carry the image, odd
|
||
rows carry a `0x55` mask in window areas (and `0xff` in the root). The display
|
||
**doubles the even field**, so the odd rows are never shown. A linear `peek`
|
||
reads both fields, which is why the first captures looked striped and
|
||
"line doubled".
|
||
|
||
Evidence: in the Messages area every even row is `0xff`/text and every odd row
|
||
is a uniform `0x55`; the root is `0xff` on both. After the owner set the
|
||
background to 100% white and rebooted, the root is `0xff` everywhere and the
|
||
TM's odd rows are still `0x55` — yet the screen is solid white. That proves the
|
||
odd rows are a non-displayed mask.
|
||
|
||
Correct capture:
|
||
|
||
```sh
|
||
python3 dwt/screenshot.py --host 100.103.100.103 \
|
||
--field even --stretch 1280x1024 --out screen.png
|
||
```
|
||
|
||
`dwt/evidence/real-unit-white2.png` is that capture and matches the glass.
|
||
|
||
This is a **display-fetch difference, not a CPU-emulation difference**. MAME's
|
||
driver models a linear 1bpp framebuffer, so the emulator never shows the
|
||
interleave; the real TMS34010 display controller is programmed for the doubled
|
||
even field.
|
||
|
||
### Correction: the framebuffer is 1024x2048, not line-doubled
|
||
|
||
The earlier "the display doubles the even field" reading was wrong. A **256 KiB**
|
||
read (2048 rows x 128 bytes) shows the 1024 image lines sitting on **even rows
|
||
at 1:1**, with the odd rows carrying the mask. The earlier 128 KiB read held
|
||
only the **top 512 image lines**; doubling them stretched the top half and cut
|
||
the bottom — which is exactly the "missing left/bottom" and "line doubled"
|
||
appearance.
|
||
|
||
Correct 1:1 capture:
|
||
|
||
```sh
|
||
python3 dwt/screenshot.py --host 100.103.100.103 --real --out screen.png
|
||
```
|
||
|
||
`--real` means `--rows 0:2048 --field even` and outputs **1024x1024 with no
|
||
resampling**. `dwt/evidence/real-unit-1to1.png` is that capture: Icon Box,
|
||
Terminal Manager, Memory bar, and the `Ethernet Port Error - 24` box at the
|
||
bottom — the whole screen.
|
||
|
||
`--stretch 1280x1024` is only the monitor's 5:4 scaling of the 1024-wide
|
||
framebuffer; it is **not** 1:1 and the nearest-neighbour version produces the
|
||
uneven-pixel artefacts. Use it only for an approximate visual match, not as
|
||
evidence.
|
||
|
||
### Final geometry: 1280x1024, 256-byte row stride
|
||
|
||
The real unit is **1280x1024** with a **256-byte row stride**. Each row's first
|
||
**160 bytes (1280 px)** are the image; the remaining **96 bytes** are a `0x55`
|
||
mask. The earlier "1024x2048, even rows" reading was an artifact of treating
|
||
that 96-byte gap as an alternate row.
|
||
|
||
This is why the 1024-wide reads lost the right quarter: the Icon Box really is
|
||
1280 px wide and its resize buttons are at the far right, outside a 1024-px
|
||
crop. The mask bytes are what looked like "line doubling".
|
||
|
||
Correct 1:1 capture:
|
||
|
||
```sh
|
||
python3 dwt/screenshot.py --host 100.103.100.103 --real --out screen.png
|
||
```
|
||
|
||
`--real` = width 1280, stride 256, 1024 rows, no resampling.
|
||
`dwt/evidence/real-unit-1280-1to1.png` is the full 1280x1024 screen, including
|
||
the Icon Box resize buttons and the `Ethernet Port Error - 24` box.
|
||
|
||
The emulator geometry is unchanged: 1024x1024 with a 128-byte stride.
|
||
|
||
### Background references
|
||
|
||
Both backgrounds are captured at the correct geometry:
|
||
`dwt/evidence/real-unit-1280-1to1.png` (100% white root) and
|
||
`dwt/evidence/real-unit-1280-50pct.png` (50% `1010` root, the owner's
|
||
preference). Only the root's image bytes change; the 256-byte stride and the
|
||
1280x1024 size are identical.
|
||
|
||
### X11 app on the real unit (2026-10-18)
|
||
|
||
The host-side loader ran end to end against the terminal:
|
||
|
||
```sh
|
||
python3 dwt/loader/hostload.py --host 100.103.100.103 \
|
||
--coff dwt/loader/build/xhello.coff --real
|
||
```
|
||
|
||
```
|
||
jump(malloc) -> 0x007e7bf0
|
||
COFF 774 B poked (0 differing)
|
||
loader 1220 B @ 0x01880000 (0 differing)
|
||
loader A8 = 0; entry 0x01840000; pcb 0x003f6510
|
||
xhello stage=6 strlen=4
|
||
```
|
||
|
||
Evidence: `dwt/evidence/real-unit-xhello.png` — a 400x200 WM-framed black
|
||
window over the Terminal Manager, on the real screen.
|
||
|
||
**Caveat:** the loader and app are linked at fixed addresses (`0x01880000`,
|
||
`0x01840000`). Re-running while that app is live overwrites the running code.
|
||
Power-cycle (or choose new addresses) before loading again.
|
||
|
||
### MOP console on the real unit
|
||
|
||
`dwt/mop_console.py --iface enp197s0f3u1 --mac 08:00:2b:16:4b:5a` works with no
|
||
password. `SHOW` returned:
|
||
|
||
```
|
||
Firmware Revision V2.2
|
||
Option Memory Present (SIMM 1/2/3 installed)
|
||
Image Option Not Present
|
||
Display Type Monochrome
|
||
Monitor Type VR319
|
||
```
|
||
|
||
**Monitor Type VR319** is the answer to the geometry question: the unit is in
|
||
the VR319 (19", 100 dpi) mode, not the emulator's forced VR150. That is why the
|
||
real framebuffer is 1280x1024 with the 256-byte stride.
|
||
|
||
`TEST 9 9 8` (Ethernet port test, LANCE loopback bit 3) was refused:
|
||
|
||
```
|
||
?ILLEGAL COMMAND - DWT IN USE
|
||
```
|
||
|
||
The console refuses diagnostics while the DWT has active work — our injected
|
||
xhello window is the blocker. The service guide requires no open windows before
|
||
remote tests: power-cycle (or close all session windows) and retry.
|
||
|
||
### Diagnostic results are not observable through rdebug
|
||
|
||
`dwt/mailbox_watch.py` polled the mailbox pointer (`0x27C190`) and the level-1
|
||
dispatcher slot (`0x16F0`) across a power cycle. The rdebug service was only
|
||
connectable **after** the OS was up, at which point:
|
||
|
||
```
|
||
mailbox=0x00000000 slot=0x8ac00000 (OS data)
|
||
```
|
||
|
||
During the boot it was unusable (timeouts / AssertionError). So the level-1
|
||
result is consumed before rdebug exists; the mailbox is a dialog/debug
|
||
structure, not the boot handoff. The only durable record is the **screen**:
|
||
the boot alert, and the Diagnostics dialog's on-screen results.
|
||
|
||
### Menu-driven Ethernet Address ROM test: PASS
|
||
|
||
After a clean power cycle, selecting **Ethernet Address ROM Test** in the
|
||
Diagnostics dialog completed, the dialog closed, and the Terminal Manager's
|
||
bottom notification reads:
|
||
|
||
```
|
||
All Diagnostics Passed
|
||
```
|
||
|
||
Evidence: `dwt/evidence/real-unit-romtest-result.png`.
|
||
|
||
This is the ROM checksum test (service-guide error **23** would be the
|
||
failure), and it **passes** on the real unit, consistent with MAME. The
|
||
boot-level `Ethernet Port Error - 24` is the separate AUI/transceiver
|
||
termination test, not the ROM.
|
||
|
||
The first attempt hung the Terminal Manager; this one completed. The hang is
|
||
not deterministic. The mailbox watcher's reconnect storm during boot is one
|
||
possible co-factor (it spawns a debug process per accepted connection), but the
|
||
diagnostic reset is the other; a single success does not settle which.
|
||
|
||
### NVRAM and LANCE address ROM, read over rdebug
|
||
|
||
`dwt/nvram_dump.py` reads 4 KiB at bit `0x6000000` and extracts the 0x800
|
||
settings bytes (low byte of each 16-bit word). Evidence:
|
||
`real-unit-nvram-settings.bin`, `real-unit-nvram-raw.bin`.
|
||
|
||
Printable settings runs:
|
||
|
||
```
|
||
0x26f 100.103.0.10 0x28f 172.20.51.0
|
||
0x3cf 1.1.1.1 0x3ef 8.8.8.8
|
||
0x40f retro.k-space.ee
|
||
0x50f 100.103.0.1
|
||
0x597 ROM 0x5e7 ROM
|
||
0x5eb 103.255.254:69
|
||
0x637 100.103.100.103 0x657 255.255.0.0
|
||
0x677 Messages 0x6ca / 0x6ea VTE 1
|
||
```
|
||
|
||
Both font-path fields persist: the selected `ROM` and the old TFTP
|
||
`103.255.254:69`.
|
||
|
||
The LANCE address ROM at bit `0x6800000` reads (low bytes of the words):
|
||
|
||
```
|
||
08 00 2b 16 4b 5a c1 86
|
||
```
|
||
|
||
byte-identical to the MAME model. The `Ethernet Address ROM Test` passing is
|
||
consistent with this.
|
||
|
||
### Self-contained TFTP loader on the real unit
|
||
|
||
Retargeted `tftpc.asm` to the LAN (`100.103.255.254:69`) as
|
||
`dwt/tcpip/tftpl.asm`; the RRQ name stays `dwt/tcpip/probe.bin` because the
|
||
server serves the chosen COFF for any requested name. The DOS 8.3 build name
|
||
must be <= 8 characters: `tftpc_lan` silently truncates and GSPLNK produces no
|
||
`TFTPC_LAN.OUT`; `tftpl` is safe.
|
||
|
||
```
|
||
server: dwt/tcpip/serve_probe.py 100.103.255.254 --file dwt/loader/build/xhello.coff
|
||
RRQ: 100.103.100.103:1036 requested 'dwt/tcpip/probe.bin' (774 bytes)
|
||
fetch: sha256 a8b3014d27deb97adcebb418a5d14da51fc3c11559e84923ede72e260d3e7271
|
||
loader: status 0, entry 0x01840000, pcb 0x00849a10
|
||
app: xhello stage=6 strlen=4
|
||
```
|
||
|
||
Evidence: `dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png`.
|
||
|
||
This is the first fully self-contained delivery on the real unit: the terminal
|
||
fetched the COFF over TFTP into `malloc` memory and ran it in a created
|
||
process, with only the rdebug jump used to start the fetch.
|
||
|
||
### Full ROM dump: byte-identical to the reference image
|
||
|
||
`dwt/rom_dump.py` read 1,835,008 bytes at bit `0x3F200000` (1792 x 1 KiB
|
||
chunks, 2:06, ~14.6 KB/s) and compared the md5:
|
||
|
||
```
|
||
b46a3e442ccb86134d2356bd70d22fd2 (real unit)
|
||
b46a3e442ccb86134d2356bd70d22fd2 (vt1200_v22.bin)
|
||
MATCH
|
||
```
|
||
|
||
So the image the project has analysed and emulated is the ROM on the machine;
|
||
no revision or bank difference. The dump is reproducible with:
|
||
|
||
```sh
|
||
python3 dwt/rom_dump.py --host 100.103.100.103
|
||
```
|
||
|
||
### X11 text app on the real unit (`xtext`)
|
||
|
||
`dwt/gui/xtext.asm` is `xhello` plus the text path: `XOpenDisplay("0::0")`,
|
||
`XCreateSimpleWindow` (400x200 at 100,100, black client area), `XLoadFont`,
|
||
`XCreateGC`, `XSetForeground(1)`, `XMapWindow`, then four iterations of
|
||
`XSync` + `XDrawString("HELLO VT1200")` + `XDrawString("text works")` and a
|
||
final `XSync`. Linked at bit `0x01840000`; loaded with
|
||
`dwt/loader/hostload.py --real` and with `--tftp` (below).
|
||
|
||
Two real-unit loads (pcb `0x00846410`, `0x00843950`) and two TFTP runs
|
||
(pcb `0x008458d0`, `0x0083f5d0`) all returned loader `A8=0`, `entry
|
||
0x01840000`, and left `stage=9`, `strlen=4`, `draw1=0x0c`, `draw2=0x0a`
|
||
(the two `XDrawString` return values are the string lengths). Text is visible
|
||
in `dwt/evidence/real-unit-xtext.png`, `real-unit-xtext-2.png`,
|
||
`real-unit-xtext-tftp.png`, and `real-unit-xtext-tftp2.png`; the same app
|
||
renders in MAME (`dwt/loader/evidence/real-unit-xtext/emulator-xtext-run{1,2}.png`).
|
||
**Several windows are visible in the later screenshots because the owner moved
|
||
windows around**; the app window is identified by its content
|
||
(`HELLO VT1200` / `text works`) and its `stage=9` markers, not by counting
|
||
windows.
|
||
|
||
What worked, exactly:
|
||
|
||
* **Font:** `"fixed"` returns **0** from `XLoadFont` on this server (the
|
||
string exists in ROM as the default-font name, but is not an openable
|
||
resource). The ROM's built-in DEC font works:
|
||
`-DEC-Terminal-Medium-R-Normal--14-140-75-75-C-8-ISO8859-1`
|
||
(`font` marker `0x00850e90`). `XLoadFont` is called for evidence; the GC
|
||
does not need the id.
|
||
* **GC:** `XCreateGC(d,win,0,NULL)` works. Passing `mask=GCFont (0x4000)`
|
||
with an `XGCValues` block (font at byte +60) **hung the connection**: the
|
||
app reached `stage=7` (mapped) but the first `XSync` never returned and
|
||
`draw1` stayed 0. With mask 0 the server fills the GC from the screen's
|
||
default GC, whose font is usable, and the draws return 12/10.
|
||
* **Window timing:** one early draw can be lost when the WM reparents the
|
||
window. The working shape draws repeatedly, with an `XSync` before each
|
||
pair; four iterations were enough (48 also ran, but the first was still
|
||
inside `XSync` at the inspection frame).
|
||
* **Saved SP/A14 in RAM is a trap.** The first version saved entry SP/A14 at
|
||
`0x01894500/20` and reloaded them after every call. The OS heap overwrote
|
||
those cells (a string `Exiting\n` landed on them), the next call restored
|
||
SP from garbage and the app died at `XCreateGC`. The callee's `rets 2`
|
||
already restores SP and A14, so the working version does not restore them
|
||
from RAM at all. Diagnostic markers were moved into zero padding at the end
|
||
of the app's own `.text` (`0x01842000`); the earlier cells at `0x01895000`
|
||
were also overwritten by the heap.
|
||
* **ROM unchanged:** `md5 b46a3e442ccb86134d2356bd70d22fd2`.
|
||
|
||
Self-contained TFTP delivery of the text app: `dwt/tcpip/tftpr.asm` is
|
||
`tftpc.asm` retargeted to server `100.103.255.254:69` and RRQ `xtext.coff`
|
||
(20-byte request: `xtext.coff\0octet\0`). Served with
|
||
`dwt/tcpip/serve_probe.py 100.103.255.254 --file dwt/loader/build/xtext.coff`
|
||
and loaded with `hostload.py --tftp --real`. Two runs, both verified
|
||
byte-for-byte (`sha256 4f713d33be8a36dfa14be8a3e4a0451bcabc1a12b1b40ddca385697104a63b26`):
|
||
|
||
```
|
||
17:26:19 RRQ 100.103.100.103:1071 requested 'xtext.coff' (1494 bytes)
|
||
17:26:55 RRQ 100.103.100.103:1081 requested 'xtext.coff' (1494 bytes)
|
||
```
|
||
|
||
The build name must be <= 8 characters: `tftpc_real` silently fails (no
|
||
`TFTPC_REAL.OUT`); `tftpr` is safe.
|
||
|
||
Evidence: `dwt/loader/evidence/real-unit-xtext/` (loads, markers, server logs,
|
||
screenshots, emulator runs, `xtext_probe.lua`).
|
||
|
||
### Bottom diagnostics box: live state (2026-10-18, read-only)
|
||
|
||
The separate bottom X window is owned by the Terminal Manager; its window id is
|
||
the global at bit `0xB7C60`. The text is selected from the status mask at bit
|
||
`0xCB640` and a pointer list at bit `0x19F150` (20-byte entries, string pointer
|
||
at +8 bytes). Live reads, two samples 2 s apart:
|
||
|
||
```
|
||
peek 0xCB640 4 -> 00 00 00 00 mask 0 -> "All Diagnostics Passed"
|
||
peek 0xB7C60 4 -> 2d 00 10 00 box window 0x0010002d
|
||
peek 0x19F190 4 -> a0 b9 4a 3f list[0] -> 0x3F4AB9A0
|
||
peek 0x19F230 4 -> 00 00 00 00 null terminator
|
||
peek 0x19FF10 2 -> 00 00 language 0 (English)
|
||
peek 0x19E8D0 4 -> 90 b0 4a 3f record 8: "Ethernet Port Error - 24"
|
||
peek 0x27C190 4 -> 00 00 00 00 DIAGNOSTIC MAILBOX pointer is NULL
|
||
peek 0x3F4AB9A0 -> "All Diagnostics Passed"
|
||
```
|
||
|
||
The mask bit mapping (bit 15 = Ethernet Port Error - 24, bit 8 = Video RAM
|
||
Error - 22, ...) was verified dynamically in MAME by setting `@0xCB640` before
|
||
the builder at frame 569. Full trace: `dwt/notes/MESSAGEBOX.md`.
|