Files
Sprinter-SDCC/toolchain/mame_interactive.py
T
snark13 72ce66275e libbgi: спрайтовый движок v2 + accel-блит/heal leaf'ы + noclip-путь
Спрайтовая графика поверх accel block-copy (docs/sprite-api-design.md):

- Ядро блиттинга: leaf'ы _bgi_blit_rows_raw (dst фикс, только src-страйд) /
  _bgi_copy_rows_raw (getimage) / _bgi_heal_rows_raw (src==dst). DI один на
  спрайт (санкция: малый спрайт под одним DI аудио не рвёт); src[0]-фикс
  снят (точная MAME подавляет CPU-байт триггера записи — на железе
  перепроверить; для heal был избыточен и снят безусловно).
- Общие bracket-free ядра _gfx_blit_full/_gfx_heal_full (полная ширина:
  клип по экрану + split >256 для putimage) + лин _gfx_blit_sprite/
  _gfx_heal_sprite (кадр ≤64, без split, 8-бит w/h) + noclip-варианты
  (клип-кода нет → полный codegen-win).  Имя *_full (не *_clip) — «clip»
  двусмысленно (sprite-ядра тоже клипуют; различитель — ширина/split).
- Движок retained-модели <sprite.h>: sprite_init/update/flip + inline
  move/frame/show/hide/touch; drawn[2] per-page внутри структуры; кадр —
  двухпроходно heal ВСЕ -> блит ВСЕ под одной W3-скобкой/банком на проход.
- Флаг gfx_sprite_clip(on/off): приложение, само следящее за границами,
  отключает клип (~+19% на анимации; диспетч пока через if — funcptr далее).
- putsprite/movesprite/gfx_blit/putimage(COPY)/getimage переведены на ядро.

Тесты: examples/balls (движок, дабл-буфер, boundary-тест клипа),
tests/sprites (RAM PASS, клип 4 края, атлас), tests/blitw (trig-leak),
tests/spriteclip (hardware-probe: железо НЕ режет за краем -> клип нужен),
tests/blitperf, tests/gfxbanks. size-baseline обновлён (53 программы).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:51:52 +03:00

315 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
r"""
mame_interactive.py — автотест .exe в MAME без участия человека.
Полный справочник по автотестированию в MAME: docs/mame-autotest.md.
Инструмент запускает Sprinter в MAME, вводит команды в DSS эмуляцией
нажатий клавиш, снимает скриншоты в заданные моменты и завершает сессию
по таймауту. Ввод идёт в НАСТОЯЩУЮ клавиатуру DSS — AT/PS-2 устройство
`:kbd:ms_naturl` (подключено последовательно к SIO Z84C015): через Lua
`ioport.ports[":kbd:ms_naturl:PX.Y"].fields[...]:set_value(1/0)` жмём/
отпускаем поля, at_keyboard сам генерит scancode'ы → SIO → DSS.
(ZX-матрица `IO_LINE0..7` и natkeyboard/-autoboot_command командную
строку DSS НЕ достают — не использовать.)
Тонкости (детали — в docs/mame-autotest.md):
- attotime.seconds — ЦЕЛОЕ; субсекундный тайминг = seconds +
attoseconds/1e18.
- клавишу держать коротко (~0.06с), между символами ~0.14с — иначе
автоповтор (typematic) и слипание scancode'ов.
- загрузка должна встать на приглашении C:\> (system.bat не должен
автозапускать Flex Navigator/приложение).
- перед запуском проверяем, что нет висящих копий MAME.
Использование:
python3 toolchain/mame_interactive.py [exe] [--data f ...] \
[--launch-at T] [--step "T:TEXT" ...] [--snap t1,t2,...] [--timeout N]
exe — .exe, который кладётся на дискету A: и АВТОМАТИЧЕСКИ
запускается (печатается a:\<EXE> + Enter в момент
--launch-at). Без exe работаем на голой строке.
--data f ... — доп. файлы на дискету A:.
--launch-at T — секунда, когда печатается запуск exe (по умолч. 8).
--step "T:TEXT" — в момент T сек напечатать TEXT (можно много раз —
диалог с уже запущенной программой). В TEXT: \n =
Enter, \t = Tab; заглавные/символы с Shift — авто.
--snap — секунды для скриншотов (по умолчанию — авто).
--timeout — секунд эмуляции до выхода.
Примеры:
# запустить тест и снять экран
python3 toolchain/mame_interactive.py tests/rt_test/rt_test.exe \
--snap 12,14 --timeout 16
# набрать команду на голой командной строке
python3 toolchain/mame_interactive.py --step "8:dir\n" \
--snap 10,11 --timeout 12
# запустить программу и ответить на её ввод
python3 toolchain/mame_interactive.py tests/menu/menu.exe \
--step "13:2\n" --snap 15 --timeout 17
"""
import argparse
import os
import shutil
import subprocess
import sys
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAME_DIR = os.path.join(PROJECT_ROOT, "mame", "v306")
IMG_DIR = os.path.join(MAME_DIR, "IMG")
MC_IMG = os.path.join(IMG_DIR, "mc.img")
SNAP_DIR = os.path.join(MAME_DIR, "snap_auto")
LUA_SCRIPT = os.path.join(MAME_DIR, "_interactive_gen.lua")
COMMON_ARGS = [
"-skip_gameinfo", "-video", "opengl", "-window", "-nofilter",
"-beta:wd179x:0", "35hd", "-beta:wd179x:1", "35hd",
"-flop1", MC_IMG, "-flop2", os.path.join(IMG_DIR, "dss171u.img"),
"-isa0", "zxbus_adapter", "-isa0:zxbus_adapter:card", "neogs",
"-hard1", os.path.join(IMG_DIR, "sp_hdd_sys.chd"),
"-hard2", os.path.join(IMG_DIR, "sp_hdd_media.chd"),
"-ata2:0", "cdrom", "-cdrom", os.path.join(IMG_DIR, "SprinterCD.iso"),
"-bios", "v3.06",
]
# --- Раскладка AT-клавиатуры :kbd:ms_naturl (tag, mask) ---
# Снято дампом ioport-полей в MAME (см. docs/mame-autotest.md).
KB = ":kbd:ms_naturl:"
PHYS = {
"a": (KB+"P1.6", 0x04), "b": (KB+"P1.5", 0x10), "c": (KB+"P1.4", 0x08),
"d": (KB+"P1.4", 0x04), "e": (KB+"P1.4", 0x20), "f": (KB+"P1.5", 0x02),
"g": (KB+"P1.5", 0x04), "h": (KB+"P2.0", 0x02), "i": (KB+"P1.4", 0x01),
"j": (KB+"P2.0", 0x04), "k": (KB+"P1.4", 0x02), "l": (KB+"P2.2", 0x08),
"m": (KB+"P2.0", 0x10), "n": (KB+"P2.0", 0x08), "o": (KB+"P2.2", 0x02),
"p": (KB+"P2.3", 0x20), "q": (KB+"P1.6", 0x02), "r": (KB+"P1.5", 0x01),
"s": (KB+"P1.2", 0x08), "t": (KB+"P1.5", 0x20), "u": (KB+"P2.0", 0x20),
"v": (KB+"P1.5", 0x08), "w": (KB+"P1.2", 0x04), "x": (KB+"P1.2", 0x10),
"y": (KB+"P2.0", 0x01), "z": (KB+"P1.6", 0x10),
"1": (KB+"P1.6", 0x01), "2": (KB+"P1.2", 0x02), "3": (KB+"P1.4", 0x40),
"4": (KB+"P1.5", 0x40), "5": (KB+"P1.5", 0x80), "6": (KB+"P2.0", 0x40),
"7": (KB+"P2.0", 0x80), "8": (KB+"P1.4", 0x80), "9": (KB+"P2.2", 0x01),
"0": (KB+"P2.3", 0x01),
"-": (KB+"P2.2", 0x20), "=": (KB+"P2.1", 0x01), "[": (KB+"P2.2", 0x04),
"]": (KB+"P2.1", 0x02), "\\": (KB+"P2.1", 0x04), ";": (KB+"P2.3", 0x02),
"'": (KB+"P2.3", 0x04), ",": (KB+"P1.4", 0x10), ".": (KB+"P2.2", 0x10),
"/": (KB+"P2.3", 0x08), "`": (KB+"P1.6", 0x80),
" ": (KB+"P2.4", 0x80), "\n": (KB+"P2.1", 0x10), "\t": (KB+"P1.6", 0x20),
}
# Символы, набираемые с Shift (значение → базовая физ. клавиша).
SHIFTED = {
"!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7",
"*": "8", "(": "9", ")": "0", "_": "-", "+": "=", "{": "[", "}": "]",
"|": "\\", ":": ";", '"': "'", "<": ",", ">": ".", "?": "/", "~": "`",
}
SHIFT_KEY = (KB+"P1.7", 0x02) # Left Shift
# Тайминг набора (сек эмуляции).
CADENCE = 0.14 # шаг между символами
HOLD = 0.06 # длительность удержания клавиши
def basename_83(name):
"""Обрезать имя файла до формата 8.3, как в FAT12 корневом каталоге."""
name = name.upper()
if '.' in name:
base, ext = name.rsplit('.', 1)
base = base[:8].ljust(8, ' ')
ext = ext[:3].ljust(3, ' ')
return f"{base}.{ext}"
return name[:8].ljust(8, ' ') + '.EXE'
def resolve(ch):
"""char → (tag, mask, need_shift). Заглавные буквы = shift+строчная."""
if ch in PHYS:
return PHYS[ch][0], PHYS[ch][1], False
if ch.isalpha() and ch.lower() in PHYS: # заглавные A-Z
t, m = PHYS[ch.lower()]
return t, m, True
if ch in SHIFTED:
t, m = PHYS[SHIFTED[ch]]
return t, m, True
raise ValueError(f"нет раскладки для символа {ch!r}")
def build_events(steps):
"""steps: список (t0, text) → плоский список событий (t, tag, mask, val)."""
ev = []
for t0, text in steps:
ct = t0
for ch in text:
tag, mask, sh = resolve(ch)
if sh:
ev.append((ct, SHIFT_KEY[0], SHIFT_KEY[1], 1))
ev.append((ct + 0.02, tag, mask, 1))
ev.append((ct + HOLD, tag, mask, 0))
ev.append((ct + HOLD+0.02, SHIFT_KEY[0], SHIFT_KEY[1], 0))
else:
ev.append((ct, tag, mask, 1))
ev.append((ct + HOLD, tag, mask, 0))
ct += CADENCE
ev.sort(key=lambda e: e[0])
return ev
def gen_lua(events, snap_times, timeout):
ev_lua = ",\n".join(
f' {{{t:.4f},"{tag}",{mask},{val}}}' for (t, tag, mask, val) in events
)
snaps = ",".join(f"{s:.4f}" for s in snap_times)
lua = f"""-- СГЕНЕРИРОВАНО mame_interactive.py — не редактировать вручную.
local function now()
local t = manager.machine.time
return t.seconds + t.attoseconds / 1e18
end
local start_time = nil
emu.add_machine_reset_notifier(function() start_time = now() end)
local events = {{
{ev_lua}
}}
local snap_times = {{{snaps}}}
local snaps_done = {{}}
local ports, idx = nil, 1
local fcache = {{}}
local function getf(tag, mask)
local key = tag .. "/" .. mask
local f = fcache[key]
if f then return f end
for _, fld in pairs(ports[tag].fields) do
if fld.mask == mask then fcache[key] = fld return fld end
end
error("нет поля " .. key)
end
emu.register_periodic(function()
if not start_time then return end
if not ports then ports = manager.machine.ioport.ports end
local e = now() - start_time
while idx <= #events and e >= events[idx][1] do
local ev = events[idx]
getf(ev[2], ev[3]):set_value(ev[4])
idx = idx + 1
end
for _, st in ipairs(snap_times) do
if not snaps_done[st] and e >= st then
snaps_done[st] = true
manager.machine.video:snapshot()
print("[interactive] snapshot t=" .. e)
end
end
if e >= {timeout} then
print("[interactive] exit t=" .. e)
manager.machine:exit()
end
end)
"""
with open(LUA_SCRIPT, "w") as f:
f.write(lua)
def check_no_stray_mame():
# Ловим только запущенный ЭМУЛЯТОР (mame/mame.arm/mame.exe c машиной
# sprinter), а не всё, где в командной строке встречается «mame»:
# голый паттерн ложно срабатывал на параллельную сборку MAME из
# исходников (clang/make с путями .../src/mame/...).
out = subprocess.run(["pgrep", "-fl", r"mame[^ ]* sprinter( |$)"],
capture_output=True, text=True).stdout
if out.strip():
print("ОШИБКА: есть запущенные копии MAME — закройте их:")
print(out)
sys.exit(1)
def parse_step(s):
if ":" not in s:
raise argparse.ArgumentTypeError("шаг должен быть 'T:TEXT'")
t_str, text = s.split(":", 1)
text = text.replace("\\n", "\n").replace("\\t", "\t")
return float(t_str), text
def main():
ap = argparse.ArgumentParser()
ap.add_argument("exe", nargs="?", help=".exe на дискету A: (авто-запуск)")
ap.add_argument("--data", nargs="*", default=[], help="доп. файлы на дискету")
ap.add_argument("--launch-at", type=float, default=8.0,
help="сек, когда печатается запуск exe (по умолч. 8)")
ap.add_argument("--step", action="append", default=[], type=parse_step,
help="'T:TEXT' — в момент T сек напечатать TEXT (можно много)")
ap.add_argument("--snap", default=None, help="секунды для скриншотов, через запятую")
ap.add_argument("--timeout", type=float, default=None, help="сек эмуляции до выхода")
args = ap.parse_args()
check_no_stray_mame()
# Собираем шаги ввода: авто-запуск exe (печать пути) + пользовательские.
steps = list(args.step)
exe_path = None
if args.exe:
exe_path = os.path.abspath(args.exe)
if not os.path.isfile(exe_path):
print(f"ОШИБКА: не найден {exe_path}")
sys.exit(1)
exe_name = basename_83(os.path.basename(exe_path)).upper()
steps.append((args.launch_at, f"a:\\{exe_name}\n"))
if not steps:
print("ОШИБКА: укажите exe для запуска или хотя бы один --step")
sys.exit(1)
events = build_events(steps)
last_ev_t = events[-1][0] if events else max(t for t, _ in steps)
if args.snap:
snap_times = [float(x) for x in args.snap.split(",")]
else:
# exe: даём программе доработать (снимаем через 4 и 6 с после ввода);
# чистый ввод: сразу после последнего шага.
pad = 4.0 if exe_path else 1.5
snap_times = [last_ev_t + pad, last_ev_t + pad + 2.0]
timeout = args.timeout if args.timeout is not None else (max(snap_times) + 1.5)
gen_lua(events, snap_times, timeout)
# Дискета БЕЗ авто-запускаемого bat → приглашение C:\>.
disk_files = []
if exe_path:
disk_files.append(exe_path)
disk_files += [os.path.abspath(p) for p in args.data]
if os.path.exists(MC_IMG):
shutil.copy(MC_IMG, MC_IMG + ".bak")
make_disk_cmd = [sys.executable, os.path.join(MAME_DIR, "make_disk.py"), MC_IMG]
subprocess.run(make_disk_cmd + disk_files, check=True, cwd=MAME_DIR)
snap_sub = os.path.join(SNAP_DIR, "sprinter")
if os.path.isdir(snap_sub):
shutil.rmtree(snap_sub)
cmd = ["timeout", str(int(timeout) + 8), "./mame.arm", "sprinter"] + COMMON_ARGS + [
"-snapshot_directory", SNAP_DIR,
"-autoboot_script", LUA_SCRIPT,
]
print("Ввод по шагам:", [(round(t, 2), repr(txt)) for t, txt in steps])
print("Скриншоты (сек):", snap_times, " таймаут:", timeout)
result = subprocess.run(cmd, cwd=MAME_DIR, capture_output=True, text=True)
for line in result.stdout.splitlines():
if "[interactive]" in line:
print(line)
if result.returncode not in (0, 124):
print("STDERR:", result.stderr[-2000:])
if os.path.isdir(snap_sub):
print("Скриншоты:")
for s in sorted(os.listdir(snap_sub)):
print(" ", os.path.join(snap_sub, s))
else:
print("Скриншотов не создано.")
if __name__ == "__main__":
main()