First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
188
lenser/mame.py
Normal file
188
lenser/mame.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
"""Shared MAME launcher used by every platform.
|
||||
|
||||
All targets (C64, Atari, Apple, TI-99/4A, and any future machine) run under MAME,
|
||||
so the launch flags, autoboot handling, and a reliable hard-kill live here in one
|
||||
place.
|
||||
|
||||
Note on MAME's startup warning screen: machines whose ROMs MAME flags as imperfect
|
||||
or bad dumps (e.g. the Atari OS) show a one-key "known problems with this system"
|
||||
notice that `-skip_gameinfo` does NOT remove (it only skips the system-info
|
||||
screen). That notice is dismissed with a single keypress by whoever is watching the
|
||||
window; we cannot and do not try to defeat it programmatically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# Common flags: windowed, cropped to the screen (no cabinet bezel), no info screen.
|
||||
BASE_FLAGS = ["-window", "-nomaximize", "-artwork_crop", "-skip_gameinfo"]
|
||||
|
||||
|
||||
def have_mame() -> bool:
|
||||
return shutil.which("mame") is not None
|
||||
|
||||
|
||||
def _default_screen() -> str | None:
|
||||
"""Which host monitor MAME's window should open on (MAME's -screen value).
|
||||
|
||||
MAME (SDL) names monitors screen0, screen1, ... in host display order. We
|
||||
prefer to throw the test window onto a *secondary* monitor so it doesn't
|
||||
cover the user's primary display. Override with LENSER_MAME_SCREEN (e.g.
|
||||
"screen0" to force the primary, or "" to let MAME choose); unset auto-picks
|
||||
the second monitor when one is connected, else leaves it to MAME.
|
||||
"""
|
||||
env = os.environ.get("LENSER_MAME_SCREEN")
|
||||
if env is not None:
|
||||
return env or None
|
||||
try:
|
||||
out = subprocess.run(["xrandr", "--query"], capture_output=True,
|
||||
text=True, timeout=5).stdout
|
||||
connected = sum(1 for ln in out.splitlines() if " connected" in ln)
|
||||
if connected >= 2:
|
||||
return "screen1"
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def c64_autorun_lua(load_cmd: str = 'load "*",8,1') -> str:
|
||||
"""Lua autoboot script that types ``load_cmd``, waits for the (slow, stock
|
||||
1541) load to finish, then types RUN.
|
||||
|
||||
A single -autoboot_command "load ...\\nrun\\n" doesn't work: the RUN is typed
|
||||
up front and lost while the drive loads. Here we post the LOAD, watch the
|
||||
cursor-blink flag $CC (0 at the READY prompt, non-zero while BASIC is busy)
|
||||
go busy then idle, and only then post RUN. ($CC read via the C64 CPU ":u7".)
|
||||
"""
|
||||
return f'''local mem = manager.machine.devices[":u7"].spaces["program"]
|
||||
local nk = manager.machine.natkeyboard
|
||||
local phase, t = 0, 0
|
||||
emu.register_periodic(function()
|
||||
local now = manager.machine.time.seconds
|
||||
if phase == 0 then
|
||||
if now > 2.0 then nk:post('{load_cmd}\\n'); phase, t = 1, now end
|
||||
elseif phase == 1 then
|
||||
if now > t + 0.5 and mem:read_u8(0xCC) ~= 0 then phase = 2 end
|
||||
elseif phase == 2 then
|
||||
if mem:read_u8(0xCC) == 0 then nk:post('run\\n'); phase = 3 end
|
||||
end
|
||||
end)
|
||||
'''
|
||||
|
||||
|
||||
def pet_autorun_lua(addr: int = 1056) -> str:
|
||||
"""Lua autoboot for the PET: SYS the viewer (at $0420 = 1056) once BASIC is
|
||||
ready. Quickload puts the program in RAM but doesn't set BASIC's pointers, so
|
||||
SYS (not RUN) reliably starts the machine-language viewer."""
|
||||
return f'''local nk = manager.machine.natkeyboard
|
||||
local done = false
|
||||
emu.register_periodic(function()
|
||||
if (not done) and manager.machine.time.seconds > 3.0 then
|
||||
nk:post("sys{addr}\\n"); done = true
|
||||
end
|
||||
end)
|
||||
'''
|
||||
|
||||
|
||||
def bbc_autorun_lua(command: str = "*RUN PIC") -> str:
|
||||
"""Lua autoboot for the BBC: type ``command`` (CR-terminated) once the OS has
|
||||
booted. natkeyboard:post is reliable where -autoboot_command isn't (the '!'
|
||||
in !BOOT needs SHIFT and a single typed command races the warning screen)."""
|
||||
return f'''local done = false
|
||||
emu.register_periodic(function()
|
||||
if (not done) and manager.machine.time.seconds > 3.0 then
|
||||
manager.machine.natkeyboard:post("{command}\\r"); done = true
|
||||
end
|
||||
end)
|
||||
'''
|
||||
|
||||
|
||||
def c16_autorun_lua(addr: int = 4128) -> str:
|
||||
"""Lua autoboot for the C16: SYS the viewer once BASIC is ready. MAME's c16
|
||||
quickload copies the PRG into RAM but doesn't set BASIC's program pointers, so
|
||||
RUN finds nothing -- but the machine-language viewer is in RAM, so SYS to its
|
||||
entry ($1020 = 4128) starts it."""
|
||||
return f'''local nk = manager.machine.natkeyboard
|
||||
local done = false
|
||||
emu.register_periodic(function()
|
||||
if (not done) and manager.machine.time.seconds > 3.0 then
|
||||
nk:post("sys{addr}\\n"); done = true
|
||||
end
|
||||
end)
|
||||
'''
|
||||
|
||||
|
||||
def coco3_rgb_lua() -> str:
|
||||
"""Lua that flips the CoCo 3's 'Monitor Type' config from Composite (the MAME
|
||||
default, an artifact-colour palette) to RGB, so the GIME shows the clean
|
||||
digital RGB palette the encoder targets. (The cartridge still autostarts.)"""
|
||||
return '''local done = false
|
||||
emu.register_periodic(function()
|
||||
if not done then
|
||||
local p = manager.machine.ioport.ports[":screen_config"]
|
||||
if p and p.fields["Monitor Type"] then
|
||||
p.fields["Monitor Type"]:set_value(1); done = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
'''
|
||||
|
||||
|
||||
def launch(machine, media, autoboot=None, autoboot_script_text=None,
|
||||
autoboot_delay=3, throttle=True, screen="auto"):
|
||||
"""Start MAME on `machine`.
|
||||
|
||||
media list of (slot, path) pairs, e.g. [("flop", disk)].
|
||||
autoboot keystrokes typed after `autoboot_delay` seconds. MAME wants
|
||||
the literal escape "\\n" (backslash+n) for RETURN.
|
||||
autoboot_script_text Lua driven startup (used for the C64 LOAD-then-RUN dance);
|
||||
written to a temp file and passed as -autoboot_script.
|
||||
throttle False adds -nothrottle so slow emulated disk loads finish
|
||||
quickly (fine for a static image; leave True for real-time).
|
||||
screen host monitor for MAME's window. "auto" (default) puts it on
|
||||
a secondary monitor when present (see _default_screen / the
|
||||
LENSER_MAME_SCREEN env); a literal "screenN" forces one;
|
||||
None lets MAME decide.
|
||||
"""
|
||||
exe = shutil.which("mame")
|
||||
if not exe:
|
||||
raise RuntimeError("MAME was not found on PATH (sudo apt install mame).")
|
||||
cmd = [exe, machine, *BASE_FLAGS]
|
||||
if screen == "auto":
|
||||
screen = _default_screen()
|
||||
if screen:
|
||||
cmd += ["-screen", screen]
|
||||
for slot, path in media:
|
||||
cmd += [f"-{slot}", os.path.abspath(path)]
|
||||
if not throttle:
|
||||
cmd.append("-nothrottle")
|
||||
if autoboot_script_text:
|
||||
fd, sp = tempfile.mkstemp(suffix=".lua", prefix="lenser_")
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(autoboot_script_text)
|
||||
cmd += ["-autoboot_delay", str(autoboot_delay), "-autoboot_script", sp]
|
||||
elif autoboot:
|
||||
cmd += ["-autoboot_delay", str(autoboot_delay), "-autoboot_command", autoboot]
|
||||
# start_new_session => MAME leads its own process group, so kill() can take the
|
||||
# whole group (MAME spawns helper threads/children) with one signal.
|
||||
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
|
||||
def kill(proc):
|
||||
"""Force a running MAME to close. MAME can ignore a polite SIGTERM while it
|
||||
owns the display, so go straight to SIGKILL on its process group."""
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
try:
|
||||
proc.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue