mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
dwt/screenshot: real geometry is 1280x1024 with a 256-byte stride (160B image + 96B mask); --real captures the full screen
This commit is contained in:
1 parent
6adc04ba02
commit
479d822aca
3 files changed
+88
-89
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -132,3 +132,26 @@ bottom — the whole screen.
|
||||
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.
|
||||
+65
-89
@@ -4,32 +4,27 @@
|
||||
No debugger, no ROM patch, no agent on the terminal: it reads the framebuffer
|
||||
out of video RAM one `peek` at a time and writes a PNG.
|
||||
|
||||
The memory layout is the one the harness renderer uses
|
||||
(`render_active_line()` in harness/mame/vt1200.cpp):
|
||||
Two geometries exist:
|
||||
|
||||
* base bit 0x04000000 (video RAM)
|
||||
* pitch 1024 bits = 128 bytes per row
|
||||
* bit order LSB-first within each byte/word
|
||||
* active 1024 x 1024 pixels; on the emulated screen it appears at
|
||||
x 192..1215, y 36..900 (MAME-derived blanking; see --screen)
|
||||
* **Emulator (MAME)** — 1024x1024, 128-byte row stride.
|
||||
* **Real VT1200** — 1280x1024, **256-byte row stride**. Each row holds the
|
||||
1280 image pixels in its first **160 bytes**; the remaining 96 bytes are a
|
||||
0x55 mask. This is why a plain 1024-wide / 128-stride read loses the right
|
||||
256 pixels (the Icon Box resize buttons) and shows mask rows as stripes.
|
||||
|
||||
Examples:
|
||||
|
||||
# the raw 1024x1024 OS canvas
|
||||
# the real unit, 1:1 (1280x1024) -- this is the one to use there
|
||||
python3 dwt/screenshot.py --host 100.103.100.103 --real --out shot.png
|
||||
|
||||
# the emulator
|
||||
python3 dwt/screenshot.py --host 192.168.99.2 --out shot.png
|
||||
|
||||
# compose the emulator's visible 1280x901 screen, for comparison with MAME dumps
|
||||
python3 dwt/screenshot.py --host 192.168.99.2 --screen --out screen.png
|
||||
# explicit geometry, if it ever differs
|
||||
python3 dwt/screenshot.py --host HOST --width 1280 --stride 256
|
||||
|
||||
# only the top of the canvas, and keep the raw framebuffer too
|
||||
python3 dwt/screenshot.py --host 192.168.99.2 --rows 0:256 \\
|
||||
--out top.png --raw vram.bin
|
||||
|
||||
# a second grab 1 s later (tearing check)
|
||||
python3 dwt/screenshot.py --host 192.168.99.2 --repeat 2 --interval 1.0
|
||||
|
||||
The peek limit is 1024 bytes, i.e. exactly 8 rows per request, so a full
|
||||
1024-row grab is 128 round trips.
|
||||
python3 dwt/screenshot.py --host HOST --real --rows 0:256 --raw vram.bin
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -42,16 +37,14 @@ import zlib
|
||||
import exploit
|
||||
|
||||
VRAM_BASE_BITS = 0x04000000
|
||||
PITCH_BITS = 1024
|
||||
ROW_BYTES = PITCH_BITS // 8 # 128
|
||||
WIDTH = 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
|
||||
HEIGHT = 1024 # image lines in both geometries
|
||||
WIDTH_DEFAULT = 1024
|
||||
STRIDE_DEFAULT = 128
|
||||
REAL_WIDTH = 1280
|
||||
REAL_STRIDE = 256
|
||||
CHUNK = exploit.MAX_XFER # 1024 bytes per peek
|
||||
|
||||
# Defaults for the emulated screen; the real unit's blanking may differ, which
|
||||
# is why the raw canvas is the primary artifact and --screen is opt-in.
|
||||
# Emulator screen composition (MAME's derived 1280x901 raster).
|
||||
SCREEN_W = 1280
|
||||
SCREEN_H = 901
|
||||
OFFSET_X = 192
|
||||
@@ -69,45 +62,27 @@ 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 <= MAX_ROWS):
|
||||
if not (0 <= start < end <= HEIGHT):
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"rows must satisfy 0 <= start < end <= {MAX_ROWS}")
|
||||
f"rows must satisfy 0 <= start < end <= {HEIGHT}")
|
||||
return start, end
|
||||
|
||||
|
||||
def decode(data: bytes, width: int = WIDTH) -> bytearray:
|
||||
def decode(data: bytes, width: int, stride: int) -> bytearray:
|
||||
"""LSB-first bits -> 8-bit grayscale (bit 1 = white)."""
|
||||
rows = len(data) // ROW_BYTES
|
||||
rows = len(data) // stride
|
||||
out = bytearray(rows * width)
|
||||
for r in range(rows):
|
||||
src = r * ROW_BYTES
|
||||
src = r * stride
|
||||
dst = r * width
|
||||
for x in range(width):
|
||||
v = (data[src + (x >> 3)] >> (x & 7)) & 1
|
||||
out[dst + x] = 255 if v else 0
|
||||
return out
|
||||
|
||||
|
||||
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 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
|
||||
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]
|
||||
b = data[src + (x >> 3)]
|
||||
out[dst + x] = 255 if ((b >> (x & 7)) & 1) else 0
|
||||
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."""
|
||||
out = bytearray(dw * dh)
|
||||
for y in range(dh):
|
||||
sy = y * sh // dh
|
||||
@@ -118,24 +93,24 @@ def scale_nearest(gray: bytearray, sw: int, sh: int,
|
||||
return out
|
||||
|
||||
|
||||
def compose_screen(gray: bytearray, rows: int, sw: int, sh: int,
|
||||
def compose_screen(gray: bytearray, rows: int, src_w: int, sw: int, sh: int,
|
||||
ox: int, oy: int) -> bytearray:
|
||||
canvas = bytearray(sw * sh)
|
||||
for r in range(rows):
|
||||
y = r + oy
|
||||
if 0 <= y < sh:
|
||||
xs = max(0, ox)
|
||||
xe = min(sw, ox + WIDTH)
|
||||
xe = min(sw, ox + src_w)
|
||||
if xe > xs:
|
||||
canvas[y * sw + xs:y * sw + xe] = \
|
||||
gray[r * WIDTH + (xs - ox):r * WIDTH + (xe - ox)]
|
||||
gray[r * src_w + (xs - ox):r * src_w + (xe - ox)]
|
||||
return canvas
|
||||
|
||||
|
||||
def write_png(path: str, width: int, height: int, gray: bytes) -> None:
|
||||
raw = bytearray()
|
||||
for y in range(height):
|
||||
raw.append(0) # filter: none
|
||||
raw.append(0)
|
||||
raw += gray[y * width:(y + 1) * width]
|
||||
comp = zlib.compress(bytes(raw), 9)
|
||||
|
||||
@@ -152,9 +127,9 @@ def write_png(path: str, width: int, height: int, gray: bytes) -> None:
|
||||
|
||||
|
||||
def grab(rd: exploit.RDebug, row_start: int, row_end: int,
|
||||
quiet: bool) -> bytes:
|
||||
start = row_start * ROW_BYTES
|
||||
end = row_end * ROW_BYTES
|
||||
stride: int, quiet: bool) -> bytes:
|
||||
start = row_start * stride
|
||||
end = row_end * stride
|
||||
data = bytearray()
|
||||
pos = start
|
||||
nchunks = (end - start + CHUNK - 1) // CHUNK
|
||||
@@ -173,9 +148,9 @@ def grab(rd: exploit.RDebug, row_start: int, row_end: int,
|
||||
|
||||
|
||||
def one(rd: exploit.RDebug, a: argparse.Namespace, tag: str) -> None:
|
||||
data = grab(rd, a.rows[0], a.rows[1], a.quiet)
|
||||
rows = len(data) // ROW_BYTES
|
||||
gray = decode(data)
|
||||
data = grab(rd, a.rows[0], a.rows[1], a.stride, a.quiet)
|
||||
rows = len(data) // a.stride
|
||||
gray = decode(data, a.width, a.stride)
|
||||
if a.invert:
|
||||
gray = bytearray(255 - v for v in gray)
|
||||
|
||||
@@ -183,23 +158,23 @@ 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_select(gray, WIDTH, rows, a.field)
|
||||
rows = rows // 2
|
||||
|
||||
if a.stretch:
|
||||
dw, dh = a.stretch
|
||||
scaled = scale_nearest(gray, WIDTH, rows, dw, dh)
|
||||
scaled = scale_nearest(gray, a.width, rows, dw, dh)
|
||||
write_png(out, dw, dh, scaled)
|
||||
print(f"[+] {out}: {dw}x{dh} (nearest-neighbour from {WIDTH}x{rows})")
|
||||
print(f"[+] {out}: {dw}x{dh} (nearest-neighbour from "
|
||||
f"{a.width}x{rows})")
|
||||
elif a.screen:
|
||||
canvas = compose_screen(gray, rows, a.screen_width, a.screen_height,
|
||||
a.offset_x, a.offset_y)
|
||||
canvas = compose_screen(gray, rows, a.width, a.screen_width,
|
||||
a.screen_height, a.offset_x, a.offset_y)
|
||||
write_png(out, a.screen_width, a.screen_height, canvas)
|
||||
print(f"[+] {out}: {a.screen_width}x{a.screen_height} "
|
||||
f"(canvas {WIDTH}x{HEIGHT}, rows {a.rows[0]}..{a.rows[1]-1})")
|
||||
f"(canvas {a.width}x{rows}, rows {a.rows[0]}..{a.rows[1]-1})")
|
||||
else:
|
||||
write_png(out, WIDTH, rows, gray)
|
||||
print(f"[+] {out}: {WIDTH}x{rows} (rows {a.rows[0]}..{a.rows[1]-1})")
|
||||
write_png(out, a.width, rows, gray)
|
||||
print(f"[+] {out}: {a.width}x{rows} "
|
||||
f"(stride {a.stride}, rows {a.rows[0]}..{a.rows[1]-1})")
|
||||
|
||||
if a.raw:
|
||||
rp = a.raw
|
||||
@@ -220,20 +195,19 @@ def main(argv: list[str]) -> int:
|
||||
ap.add_argument("--timeout", type=float, default=8.0)
|
||||
ap.add_argument("--out", default="shot.png")
|
||||
ap.add_argument("--rows", type=parse_rows, default=(0, HEIGHT),
|
||||
metavar="A:B", help=f"row range to read (default 0:{HEIGHT})")
|
||||
ap.add_argument("--screen", action="store_true",
|
||||
help="compose the visible screen instead of the raw canvas")
|
||||
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="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")
|
||||
metavar="A:B", help=f"image row range (default 0:{HEIGHT})")
|
||||
ap.add_argument("--width", type=int, default=None,
|
||||
help=f"pixels per row (default {WIDTH_DEFAULT}; real {REAL_WIDTH})")
|
||||
ap.add_argument("--stride", type=int, default=None,
|
||||
help=f"bytes per framebuffer row (default {STRIDE_DEFAULT}; "
|
||||
f"real {REAL_STRIDE})")
|
||||
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)")
|
||||
help=f"real-unit preset: --width {REAL_WIDTH} "
|
||||
f"--stride {REAL_STRIDE}, 1280x1024 at 1:1")
|
||||
ap.add_argument("--stretch", type=parse_size, metavar="WxH", default=None,
|
||||
help="nearest-neighbour scale the result to WxH")
|
||||
ap.add_argument("--screen", action="store_true",
|
||||
help="compose MAME's 1280x901 emulator screen")
|
||||
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)
|
||||
@@ -246,14 +220,16 @@ 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"
|
||||
|
||||
if a.width is None:
|
||||
a.width = REAL_WIDTH if a.real else WIDTH_DEFAULT
|
||||
if a.stride is None:
|
||||
a.stride = REAL_STRIDE if a.real else STRIDE_DEFAULT
|
||||
|
||||
rd = exploit.RDebug(a.host, a.port, a.timeout, a.verbose)
|
||||
rd.connect()
|
||||
try:
|
||||
rd.liveness(7) # cheap reachability check
|
||||
rd.liveness(7)
|
||||
for i in range(max(1, a.repeat)):
|
||||
if i:
|
||||
time.sleep(a.interval)
|
||||
|
||||
Reference in new issue
Block a user