diff --git a/dwt/evidence/real-unit-1to1.png b/dwt/evidence/real-unit-1to1.png new file mode 100644 index 0000000..db1e205 Binary files /dev/null and b/dwt/evidence/real-unit-1to1.png differ diff --git a/dwt/notes/REAL_UNIT.md b/dwt/notes/REAL_UNIT.md index 135eef5..b05d258 100644 --- a/dwt/notes/REAL_UNIT.md +++ b/dwt/notes/REAL_UNIT.md @@ -107,3 +107,28 @@ 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. diff --git a/dwt/screenshot.py b/dwt/screenshot.py index 5fe82ad..04ae6f7 100644 --- a/dwt/screenshot.py +++ b/dwt/screenshot.py @@ -45,7 +45,8 @@ VRAM_BASE_BITS = 0x04000000 PITCH_BITS = 1024 ROW_BYTES = PITCH_BITS // 8 # 128 WIDTH = 1024 -HEIGHT = 1024 +HEIGHT = 1024 # image lines +MAX_ROWS = 2048 # real unit: image on even rows of 2048 CHUNK = exploit.MAX_XFER # 1024 bytes = 8 rows ROWS_PER_CHUNK = CHUNK // ROW_BYTES # 8 @@ -68,9 +69,9 @@ def parse_rows(text: str) -> tuple[int, int]: start, end = int(a, 0), int(b, 0) else: start, end = 0, int(text, 0) - if not (0 <= start < end <= HEIGHT): + if not (0 <= start < end <= MAX_ROWS): raise argparse.ArgumentTypeError( - f"rows must satisfy 0 <= start < end <= {HEIGHT}") + f"rows must satisfy 0 <= start < end <= {MAX_ROWS}") return start, end @@ -87,19 +88,19 @@ 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. +def field_select(gray: bytearray, w: int, h: int, parity: str) -> bytearray: + """De-interleave: keep one row parity, at 1:1 (no doubling). - 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. + The real unit's framebuffer is 1024x2048: the 1024 image lines are on even + rows and the odd rows carry a 0x55 mask. The display scans the even rows. + Reading all 2048 rows and keeping the even ones gives the 1024x1024 image + exactly as the hardware shows it. """ 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 + oh = h // 2 + out = bytearray(w * oh) + for y in range(oh): + sy = y * 2 + off out[y * w:(y + 1) * w] = gray[sy * w:(sy + 1) * w] return out @@ -183,7 +184,8 @@ def one(rd: exploit.RDebug, a: argparse.Namespace, tag: str) -> None: 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) + gray = field_select(gray, WIDTH, rows, a.field) + rows = rows // 2 if a.stretch: dw, dh = a.stretch scaled = scale_nearest(gray, WIDTH, rows, dw, dh) @@ -225,9 +227,13 @@ def main(argv: list[str]) -> int: 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") + help="de-interleave: keep one row parity at 1:1; the real " + "unit's 1024 image lines are on even rows of a 2048-row " + "framebuffer, odd rows are a 0x55 mask") + ap.add_argument("--real", action="store_true", + help="real-unit preset: --rows 0:2048 --field even, 1:1 " + "(add --stretch 1280x1024 only if you want the 5:4 " + "display scaling)") 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) @@ -240,6 +246,9 @@ def main(argv: list[str]) -> int: ap.add_argument("--quiet", action="store_true") ap.add_argument("--verbose", action="store_true") a = ap.parse_args(argv[1:]) + if a.real: + a.rows = (0, MAX_ROWS) + a.field = "even" rd = exploit.RDebug(a.host, a.port, a.timeout, a.verbose) rd.connect()