From 71588e5405ca2d95a414b103bfaa9d3842f63f80 Mon Sep 17 00:00:00 2001 From: SDD Date: Sun, 18 Oct 2026 16:18:27 +0300 Subject: [PATCH] dwt/screenshot: --field even for the real unit's line-interleaved framebuffer; the capture now matches the glass --- dwt/evidence/real-unit-white2.png | Bin 0 -> 8418 bytes dwt/notes/REAL_UNIT.md | 28 ++++++++++++++++++++++++++++ dwt/screenshot.py | 23 +++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 dwt/evidence/real-unit-white2.png diff --git a/dwt/evidence/real-unit-white2.png b/dwt/evidence/real-unit-white2.png new file mode 100644 index 0000000000000000000000000000000000000000..461728ad57a29fd4b4584cd43348f751e2c66545 GIT binary patch literal 8418 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 literal 0 HcmV?d00001 diff --git a/dwt/notes/REAL_UNIT.md b/dwt/notes/REAL_UNIT.md index ba2c42f..135eef5 100644 --- a/dwt/notes/REAL_UNIT.md +++ b/dwt/notes/REAL_UNIT.md @@ -79,3 +79,31 @@ 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. diff --git a/dwt/screenshot.py b/dwt/screenshot.py index 3752214..5fe82ad 100644 --- a/dwt/screenshot.py +++ b/dwt/screenshot.py @@ -87,6 +87,23 @@ def decode(data: bytes, width: int = WIDTH) -> bytearray: return out +def field_double(gray: bytearray, w: int, h: int, parity: str) -> bytearray: + """Select one row parity and duplicate each row. + + The real unit's framebuffer is line-interleaved: the image is on even rows + and the odd rows carry a 0x55 mask; the display doubles the even field. + Reading linearly shows both, which looks 'line doubled' and striped. + """ + off = 0 if parity == "even" else 1 + out = bytearray(w * h) + for y in range(h): + sy = (y // 2) * 2 + off + if sy >= h: + sy = h - 1 + out[y * w:(y + 1) * w] = gray[sy * w:(sy + 1) * w] + return out + + def scale_nearest(gray: bytearray, sw: int, sh: int, dw: int, dh: int) -> bytearray: """Nearest-neighbour scale; used for the real unit's 1024->1280 display.""" @@ -165,6 +182,8 @@ def one(rd: exploit.RDebug, a: argparse.Namespace, tag: str) -> None: if a.repeat > 1: root, dot, ext = out.rpartition(".") out = f"{root}-{tag}.{ext}" if dot else f"{out}-{tag}" + if a.field: + gray = field_double(gray, WIDTH, rows, a.field) if a.stretch: dw, dh = a.stretch scaled = scale_nearest(gray, WIDTH, rows, dw, dh) @@ -205,6 +224,10 @@ def main(argv: list[str]) -> int: ap.add_argument("--stretch", type=parse_size, metavar="WxH", default=None, help="nearest-neighbour scale the raw canvas to WxH " "(the real unit displays 1024x1024 as 1280x1024)") + ap.add_argument("--field", choices=("even", "odd"), default=None, + help="select one row parity and double it; the real unit " + "carries the image on even rows and a 0x55 mask on odd " + "rows, and the display doubles the even field") ap.add_argument("--screen-width", type=int, default=SCREEN_W) ap.add_argument("--screen-height", type=int, default=SCREEN_H) ap.add_argument("--offset-x", type=int, default=OFFSET_X)