diff --git a/dwt/evidence/real-unit-white2.png b/dwt/evidence/real-unit-white2.png new file mode 100644 index 0000000..461728a Binary files /dev/null and b/dwt/evidence/real-unit-white2.png differ 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)